1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import ExpoModulesCore
4 import React
5 
6 #if DEBUG && EX_DEV_CLIENT_NETWORK_INSPECTOR
7 
8 var isHookInstalled = false
9 
10 @objc(EXDevLauncherNetworkInterceptor)
11 public final class DevLauncherNetworkInterceptor: NSObject, ExpoRequestCdpInterceptorDelegate {
12   fileprivate static var inspectorPackagerConn: RCTInspectorPackagerConnection?
13 
14   public override init() {
15     super.init()
16     assert(Thread.isMainThread)
17 
18     if !isHookInstalled {
19       EXDevLauncherUtils.swizzleClassMethod(
20         selector: #selector(RCTInspectorDevServerHelper.connect(withBundleURL:)),
21         withSelector: #selector(RCTInspectorDevServerHelper.EXDevLauncher_connect(withBundleURL:)),
22         forClass: RCTInspectorDevServerHelper.self
23       )
24       EXDevLauncherUtils.swizzleClassMethod(
25         selector: #selector(getter: URLSessionConfiguration.default),
26         withSelector: #selector(URLSessionConfiguration.EXDevLauncher_urlSessionConfiguration),
27         forClass: URLSessionConfiguration.self
28       )
29       isHookInstalled = true
30     }
31 
32     ExpoRequestCdpInterceptor.shared.setDelegate(self)
33   }
34 
35   deinit {
36     assert(Thread.isMainThread)
37     ExpoRequestCdpInterceptor.shared.setDelegate(nil)
38   }
39 
40   // MARK: ExpoRequestCdpInterceptorDelegate implementations
41 
dispatchnull42   public func dispatch(_ event: String) {
43     Self.inspectorPackagerConn?.sendWrappedEventToAllPages(event)
44   }
45 }
46 
47 extension RCTInspectorDevServerHelper {
48   private typealias ConnectFunc = @convention(c) (AnyObject, Selector, URL)
49     -> RCTInspectorPackagerConnection?
50 
51   /**
52    Swizzled `RCTInspectorDevServerHelper.connect(withBundleURL:)` for us to get the `RCTInspectorPackagerConnection` instance
53    */
54   @objc
EXDevLauncher_connectnull55   static func EXDevLauncher_connect(withBundleURL bundleURL: URL)
56     -> RCTInspectorPackagerConnection? {
57     let inspectorPackagerConn = try? EXDevLauncherUtils.invokeOriginalClassMethod(
58       selector: #selector(RCTInspectorDevServerHelper.connect(withBundleURL:)),
59       forClass: RCTInspectorDevServerHelper.self,
60       A0: bundleURL
61     ) as? RCTInspectorPackagerConnection
62 
63     // Exclude the connections for dev-client bundles
64     if !bundleURL.absoluteString.starts(with: Bundle.main.bundleURL.absoluteString) {
65       DevLauncherNetworkInterceptor.inspectorPackagerConn = inspectorPackagerConn
66     }
67     return inspectorPackagerConn
68   }
69 }
70 
71 extension RCTInspectorPackagerConnection {
72   /**
73    Indicates whether the packager connection is established and ready to send messages
74    */
isReadyToSendnull75   func isReadyToSend() -> Bool {
76     guard isConnected() else {
77       return false
78     }
79     guard let webSocket = value(forKey: "_webSocket") as? AnyObject,
80       let readyState = webSocket.value(forKey: "readyState") as? Int else {
81       return false
82     }
83     // To support both RCTSRWebSocket (RN < 0.72) and SRWebSocket (RN >= 0.72)
84     // and not to introduce extra podspec dependencies,
85     // we use the internal and hardcoded value here.
86     // Given the fact that both RCTSRWebSocket and SRWebSocket has the readyState property
87     // and the open state is 1.
88     let OPEN_STATE = 1
89     return readyState == OPEN_STATE
90   }
91 
92   /**
93    Sends message from native to inspector proxy
94    */
sendWrappedEventToAllPagesnull95   func sendWrappedEventToAllPages(_ event: String) {
96     guard isReadyToSend() else {
97       return
98     }
99     for page in RCTInspector.pages() where !page.title.contains("Reanimated") {
100       perform(NSSelectorFromString("sendWrappedEvent:message:"), with: String(page.id), with: event)
101     }
102   }
103 }
104 
105 extension URLSessionConfiguration {
106   private typealias GetterFunc = @convention(c) (AnyObject, Selector) -> URLSessionConfiguration
107 
108   /**
109    Swizzled `URLSessionConfiguration.default` for us to add the `EXDevLauncherRequestLoggerProtocol` interceptor
110    */
111   @objc
EXDevLauncher_urlSessionConfigurationnull112   static func EXDevLauncher_urlSessionConfiguration() -> URLSessionConfiguration {
113     guard let config = try? EXDevLauncherUtils.invokeOriginalClassMethod(
114       selector: #selector(getter: URLSessionConfiguration.default),
115       forClass: URLSessionConfiguration.self
116     ) as? URLSessionConfiguration else {
117       fatalError("Unable to get original URLSessionConfiguration.default")
118     }
119     var protocolClasses: [AnyClass] = config.protocolClasses ?? []
120     if !protocolClasses.contains(where: { $0 == ExpoRequestInterceptorProtocol.self }) {
121       protocolClasses.insert(ExpoRequestInterceptorProtocol.self, at: 0)
122     }
123     config.protocolClasses = protocolClasses
124     return config
125   }
126 }
127 
128 #else
129 
130 @objc(EXDevLauncherNetworkInterceptor)
131 public final class DevLauncherNetworkInterceptor: NSObject {
132 }
133 
134 #endif
135