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