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