diff --git a/README.md b/README.md index 0850d43..02b5ce5 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,17 @@ NetShears adds a Request interceptor mechanisms to be able to modify the HTTP/HT NetShears has three main functionality : -1 - Request interceptor mechanisms to be able to modify the HTTP/HTTPS Request before being sent. +1 - Network request observer which can be used to observe every HTTP/HTTPS request using delegation. +```swift +Netshears.shared.startListener() +``` + +2 - Request interceptor mechanisms to be able to modify the HTTP/HTTPS Request before being sent. ```swift Netshears.shared.startInterceptor() ``` -2 - Show network traffics. +3 - Show network traffics. ```swift Netshears.shared.startLogger() ``` @@ -131,6 +136,16 @@ NetShears.shared.ignore = .enabled(ignoreHandler: { request in Note that requests will be ignored **just** in Traffic Monitoring View; so you can set another ```ignoreHandler``` and get different results. By default ```NetShears.ignore``` is ```.disabled```. +# Request Observer + +For observing requests you need to first call startListener then just simply adopt RequestBroadcast delegate. +```swift +NetShears.shared.startListener() + +RequestBroadcast.shared.setDelegate(self) + +``` + ## Installation ### [Swift Package Manager](https://github.com/apple/swift-package-manager) diff --git a/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift b/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift index 376e5df..2584e4a 100644 --- a/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift +++ b/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift @@ -14,12 +14,14 @@ extension URLSessionConfiguration { return [] } var originalProtocolClasses = fakeProcotolClasses.filter { - return $0 != NetworkInterceptorUrlProtocol.self && $0 != NetworkLoggerUrlProtocol.self + return $0 != NetworkInterceptorUrlProtocol.self && $0 != NetworkLoggerUrlProtocol.self && $0 != NetwrokListenerUrlProtocol.self } if NetShears.shared.loggerEnable { originalProtocolClasses.insert(NetworkLoggerUrlProtocol.self, at: 0) } - + if NetShears.shared.listenerEnable { + originalProtocolClasses.insert(NetwrokListenerUrlProtocol.self, at: 0) + } if NetShears.shared.interceptorEnable { originalProtocolClasses.insert(NetworkInterceptorUrlProtocol.self, at: 0) } diff --git a/Sources/NetShears/NetShears.swift b/Sources/NetShears/NetShears.swift index 528fb1a..7620d1a 100644 --- a/Sources/NetShears/NetShears.swift +++ b/Sources/NetShears/NetShears.swift @@ -30,6 +30,7 @@ public final class NetShears: NSObject { internal var loggerEnable = false internal var interceptorEnable = false + internal var listenerEnable = false internal var swizzled = false let networkRequestInterceptor = NetworkRequestInterceptor() @@ -65,6 +66,16 @@ public final class NetShears: NSObject { self.networkRequestInterceptor.stopLogger() checkSwizzling() } + + public func startListener() { + self.networkRequestInterceptor.startListener() + checkSwizzling() + } + + public func stopListener() { + self.networkRequestInterceptor.stopListener() + checkSwizzling() + } public func modify(modifier: Modifier) { config.addModifier(modifier: modifier) @@ -120,7 +131,9 @@ public final class NetShears: NSObject { RequestStorage.shared.newRequestArrived(request) } - RequestBroadcast.shared.newRequestArrived(request) + if listenerEnable { + RequestBroadcast.shared.newRequestArrived(request) + } } public func addGRPC(url: String, diff --git a/Sources/NetShears/NetworkRequestInterceptor.swift b/Sources/NetShears/NetworkRequestInterceptor.swift index 5b7d82d..c70232e 100644 --- a/Sources/NetShears/NetworkRequestInterceptor.swift +++ b/Sources/NetShears/NetworkRequestInterceptor.swift @@ -38,6 +38,16 @@ import Foundation NetShears.shared.loggerEnable = false URLProtocol.unregisterClass(NetworkLoggerUrlProtocol.self) } + + func startListener() { + NetShears.shared.listenerEnable = true + URLProtocol.registerClass(NetwrokListenerUrlProtocol.self) + } + + func stopListener() { + NetShears.shared.listenerEnable = false + URLProtocol.unregisterClass(NetwrokListenerUrlProtocol.self) + } } diff --git a/Sources/NetShears/URLProtocol/NetwrokListenerUrlProtocol.swift b/Sources/NetShears/URLProtocol/NetwrokListenerUrlProtocol.swift new file mode 100644 index 0000000..edb4441 --- /dev/null +++ b/Sources/NetShears/URLProtocol/NetwrokListenerUrlProtocol.swift @@ -0,0 +1,148 @@ +// +// NetwrokListenerUrlProtocol.swift +// NetShears +// +// Created by Mehdi Mirzaie on 6/9/21. +// + +import Foundation + +class NetwrokListenerUrlProtocol: URLProtocol { + + struct Constants { + static let RequestHandledKey = "NetworkListenerUrlProtocol" + } + + var session: URLSession? + var sessionTask: URLSessionDataTask? + var currentRequest: NetShearsRequestModel? + lazy var requestObserver: RequestObserverProtocol = { + RequestObserver(options: [ + RequestBroadcast.shared + ]) + }() + + override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { + super.init(request: request, cachedResponse: cachedResponse, client: client) + + if session == nil { + session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) + } + } + + override class func canInit(with request: URLRequest) -> Bool { + + if NetwrokListenerUrlProtocol.property(forKey: Constants.RequestHandledKey, in: request) != nil { + return false + } + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + let newRequest = ((request as NSURLRequest).mutableCopy() as? NSMutableURLRequest)! + NetwrokListenerUrlProtocol.setProperty(true, forKey: Constants.RequestHandledKey, in: newRequest) + sessionTask = session?.dataTask(with: newRequest as URLRequest) + sessionTask?.resume() + + currentRequest = NetShearsRequestModel(request: newRequest, session: session) + if let request = currentRequest { + requestObserver.newRequestArrived(request) + } + } + + override func stopLoading() { + sessionTask?.cancel() + currentRequest?.httpBody = body(from: request) + if let startDate = currentRequest?.date{ + currentRequest?.duration = fabs(startDate.timeIntervalSinceNow) * 1000 //Find elapsed time and convert to milliseconds + } + currentRequest?.isFinished = true + + if let request = currentRequest { + requestObserver.newRequestArrived(request) + } + session?.invalidateAndCancel() + } + + private func body(from request: URLRequest) -> Data? { + /// The receiver will have either an HTTP body or an HTTP body stream only one may be set for a request. + /// A HTTP body stream is preserved when copying an NSURLRequest object, + /// but is lost when a request is archived using the NSCoding protocol. + return request.httpBody ?? request.getHttpBodyStreamData() + } + + deinit { + session = nil + sessionTask = nil + currentRequest = nil + } +} + +extension NetwrokListenerUrlProtocol: URLSessionDataDelegate { + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + client?.urlProtocol(self, didLoad: data) + if currentRequest?.dataResponse == nil{ + currentRequest?.dataResponse = data + } + else{ + currentRequest?.dataResponse?.append(data) + } + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + let policy = URLCache.StoragePolicy(rawValue: request.cachePolicy.rawValue) ?? .notAllowed + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: policy) + currentRequest?.initResponse(response: response) + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let error = error { + currentRequest?.errorClientDescription = error.localizedDescription + client?.urlProtocol(self, didFailWithError: error) + } else { + client?.urlProtocolDidFinishLoading(self) + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { + client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response) + completionHandler(request) + } + + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + guard let error = error else { return } + currentRequest?.errorClientDescription = error.localizedDescription + client?.urlProtocol(self, didFailWithError: error) + } + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let protectionSpace = challenge.protectionSpace + let sender = challenge.sender + + if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let serverTrust = protectionSpace.serverTrust { + let credential = URLCredential(trust: serverTrust) + sender?.use(credential, for: challenge) + completionHandler(.useCredential, credential) + return + } + } + } + + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + client?.urlProtocolDidFinishLoading(self) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + if let url = task.currentRequest?.url { + NetShears.shared.taskProgressDelegate?.task(url, didRecieveProgress: task.progress) + } + } +} + +