1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import Foundation
4 
5 /**
6  A `URLSession` interceptor which passes network events to its delegate
7  */
8 @objc(EXRequestInterceptorProtocol)
9 public final class ExpoRequestInterceptorProtocol: URLProtocol, URLSessionDataDelegate {
10   private static let REQUEST_ID = "ExpoRequestInterceptorProtocol.requestId"
11   private static var requestIdProvider = RequestIdProvider()
12   private lazy var urlSession = URLSession(
13     configuration: URLSessionConfiguration.default,
14     delegate: self,
15     delegateQueue: nil
16   )
17   private var dataTask_: URLSessionDataTask?
18   private let responseBody = NSMutableData()
19   private var responseIsText = false
20   private var responseContentLength: Int64 = 0
21 
22   static let MAX_BODY_SIZE = 1_048_576
23 
24   // Currently keeps the delegate fixed for ExpoRequestCdpInterceptor and be thread-safe
25   static let delegate: ExpoRequestInterceptorProtocolDelegate = ExpoRequestCdpInterceptor.shared
26 
27   // MARK: URLProtocol implementations
28 
29   public override class func canInit(with request: URLRequest) -> Bool {
30     guard let scheme = request.url?.scheme else {
31       return false
32     }
33     if !["http", "https"].contains(scheme) {
34       return false
35     }
36     let isNewRequest = URLProtocol.property(
37       forKey: Self.REQUEST_ID,
38       in: request
39     ) == nil
40     return isNewRequest
41   }
42 
43   override init(
44     request: URLRequest,
45     cachedResponse: CachedURLResponse?,
46     client: URLProtocolClient?
47   ) {
48     super.init(request: request, cachedResponse: cachedResponse, client: client)
49     // swiftlint:disable force_cast
50     let mutableRequest = request as! NSMutableURLRequest
51     // swiftlint:enable force_cast
52     let requestId = Self.requestIdProvider.create()
53     URLProtocol.setProperty(
54       requestId,
55       forKey: Self.REQUEST_ID,
56       in: mutableRequest
57     )
58     Self.delegate.willSendRequest(
59       requestId: requestId,
60       request: mutableRequest as URLRequest,
61       redirectResponse: nil
62     )
63     dataTask_ = urlSession.dataTask(with: mutableRequest as URLRequest)
64   }
65 
66   public override class func canonicalRequest(for request: URLRequest) -> URLRequest {
67     request
68   }
69 
70   public override func startLoading() {
71     dataTask_?.resume()
72   }
73 
74   public override func stopLoading() {
75     dataTask_?.cancel()
76   }
77 
78   // MARK: URLSessionDataDelegate implementations
79 
80   public func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
81     client?.urlProtocol(self, didLoad: data)
82     if responseBody.length + data.count <= Self.MAX_BODY_SIZE {
83       responseBody.append(data)
84     }
85   }
86 
87   public func urlSession(
88     _: URLSession,
89     dataTask: URLSessionDataTask,
90     didReceive response: URLResponse,
91     completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
92   ) {
93     if let resp = response as? HTTPURLResponse,
94       let currentRequest = dataTask.currentRequest,
95       let requestId = URLProtocol.property(
96         forKey: Self.REQUEST_ID,
97         in: currentRequest
98       ) as? String {
99       Self.delegate.didReceiveResponse(
100         requestId: requestId,
101         request: currentRequest,
102         response: resp
103       )
104 
105       let contentType = resp.value(forHTTPHeaderField: "Content-Type")
106       responseIsText = (contentType?.starts(with: "text/") ?? false) || contentType == "application/json"
107       responseContentLength = resp.expectedContentLength
108     }
109     completionHandler(.allow)
110     client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
111   }
112 
113   public func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
114     if let error = error {
115       client?.urlProtocol(self, didFailWithError: error)
116     } else {
117       if responseContentLength > 0 && responseContentLength <= Self.MAX_BODY_SIZE,
118         let currentRequest = task.currentRequest,
119         let requestId = URLProtocol.property(
120           forKey: Self.REQUEST_ID,
121           in: currentRequest
122         ) as? String {
123         Self.delegate.didReceiveResponseBody(
124           requestId: requestId, responseBody: responseBody as Data, isText: responseIsText)
125       }
126       client?.urlProtocolDidFinishLoading(self)
127     }
128   }
129 
130   public func urlSession(
131     _: URLSession,
132     task _: URLSessionTask,
133     willPerformHTTPRedirection response: HTTPURLResponse,
134     newRequest request: URLRequest,
135     completionHandler: @escaping (URLRequest?) -> Void
136   ) {
137     if let requestId = URLProtocol.property(forKey: Self.REQUEST_ID, in: request) as? String {
138       Self.delegate.willSendRequest(
139         requestId: requestId,
140         request: request,
141         redirectResponse: response
142       )
143     }
144     completionHandler(request)
145   }
146 
147   /**
148    Data structure to save the response for redirection
149    */
150   private struct RedirectResponse {
151     let requestId: String
152     let redirectResponse: HTTPURLResponse
153   }
154 
155   /**
156    A helper class to create a unique request ID
157    */
158   private struct RequestIdProvider {
159     private var value: UInt64 = 0
160 
161     mutating func create() -> String {
162       // We could ensure the increment thread safety,
163       // because we access this function from the same thread (com.apple.CFNetwork.CustomProtocols).
164       value += 1
165       return String(value)
166     }
167   }
168 }
169 
170 /**
171  The delegate to dispatch network request events
172  */
173 @objc(EXRequestInterceptorProtocolDelegate)
174 protocol ExpoRequestInterceptorProtocolDelegate {
175   @objc
176   func willSendRequest(requestId: String, request: URLRequest, redirectResponse: HTTPURLResponse?)
177 
178   @objc
179   func didReceiveResponse(requestId: String, request: URLRequest, response: HTTPURLResponse)
180 
181   @objc
182   func didReceiveResponseBody(requestId: String, responseBody: Data, isText: Bool)
183 }
184