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