diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a4219ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing to NetShears # + +- If you **found a bug**, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02054d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Mehdi Mirzaie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..9d59b4d --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "NetShears", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "NetShears", + targets: ["NetShears"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "NetShears", + dependencies: []), + .testTarget( + name: "NetShearsTests", + dependencies: ["NetShears"]), + ] +) diff --git a/README.md b/README.md index 87cf214..55fda08 100644 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ +![Logo](./logo.png) + # NetShears + +NetShears is a Network interceptor framework written in Swift. + +NetShears adds a Request interceptor mechanisms to be able to modify the HTTPRequest before being sent . This mechanism can be used to implement authentication policies, add headers to a request , add log trace or even redirect requests. + + +## Features + +- [x] Intercept HTTP/HTTPS request header +- [x] Intercept HTTP/HTTPS request endpoint +- [ ] Intercept HTTP/HTTPS response body +- [ ] View traffic logs +- [ ] Block HTTP requets + + +## How to use +Start NetShears by calling ```startRecording()``` in didFinishLaunchingWithOptions + +```swift +import NetShears + +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + + NetShears.startRecording() + +} +``` +Header Modification: + +```swift +let header = HeaderModifyModel(key: "API-Version", value: "123") +let headerModifier = RequestEvaluatorModifierHeader(header: header) +NetShears.shared.modify(modifier: headerModifier) +``` + +Endpoint Modification: + +```swift +let endpoint = RedirectedRequestModel(originalUrl: "/register", redirectUrl: "/login") +let endpointModifier = RequestEvaluatorModifierEndpoint(redirectedRequest: endpoint) +NetShears.shared.modify(modifier: endpointModifier) +``` + +## Installation +NetShears can be used via the [Swift Package Manager](https://swift.org/package-manager/). +Just add it to the dependencies in your Package.swift file: + +```Swift +let package = Package( + name: "MyPackage", + dependencies: [ + ... + .package(url: "https://github.com/divar-ir/NetShears.git", from: "1.0.0"), + ], + ... +) +``` + + +## Contributing +Please see our [Contributing Guide](./CONTRIBUTING.md). + +## Inspiration + +* [depoon/NetworkInterceptor](https://github.com/depoon/NetworkInterceptor) + +## License +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/Sources/NetShears/Extension/URLRequest+Extension.swift b/Sources/NetShears/Extension/URLRequest+Extension.swift new file mode 100644 index 0000000..1e15947 --- /dev/null +++ b/Sources/NetShears/Extension/URLRequest+Extension.swift @@ -0,0 +1,29 @@ +// +// URLRequest+Extension.swift +// +// +// Created by Mehdi Mirzaie on 6/9/21. +// + +import Foundation + +extension URLRequest { + public func getHttpBodyStreamData() -> Data? { + guard let httpBodyStream = self.httpBodyStream else { + return nil + } + let data = NSMutableData() + var buffer = [UInt8](repeating: 0, count: 4096) + + httpBodyStream.open() + while httpBodyStream.hasBytesAvailable { + let length = httpBodyStream.read(&buffer, maxLength: 4096) + if length == 0 { + break + } else { + data.append(&buffer, length: length) + } + } + return data as Data + } +} diff --git a/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift b/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift new file mode 100644 index 0000000..6a0f060 --- /dev/null +++ b/Sources/NetShears/Extension/URLSessionConfiguration+Extension.swift @@ -0,0 +1,23 @@ +// +// URLSessionConfiguration+Extension.swift +// +// +// Created by Mehdi Mirzaie on 6/9/21. +// + +import Foundation + +extension URLSessionConfiguration { + + @objc func fakeProcotolClasses() -> [AnyClass]? { + guard let fakeProcotolClasses = self.fakeProcotolClasses() else { + return [] + } + var originalProtocolClasses = fakeProcotolClasses.filter { + return $0 != NetworkRequestSniffableUrlProtocol.self + } + originalProtocolClasses.insert(NetworkRequestSniffableUrlProtocol.self, at: 0) + return originalProtocolClasses + } + +} diff --git a/Sources/NetShears/Helpers/URLRequestFactory.swift b/Sources/NetShears/Helpers/URLRequestFactory.swift new file mode 100644 index 0000000..8053a8f --- /dev/null +++ b/Sources/NetShears/Helpers/URLRequestFactory.swift @@ -0,0 +1,21 @@ +// +// URLRequestFactory.swift +// +// +// Created by Mehdi Mirzaie on 6/4/21. +// + +import Foundation + +extension URLRequest { + mutating func modifyURLRequestEndpoint(redirectUrl: RedirectedRequestModel) { + var urlString = "\(url!.absoluteString)" + urlString = urlString.replacingOccurrences(of: redirectUrl.originalUrl, with: redirectUrl.redirectUrl) + url = URL(string: urlString)! + } + + mutating func modifyURLRequestHeader(header: HeaderModifyModel) { + setValue(header.value, forHTTPHeaderField: header.key) + } +} + diff --git a/Sources/NetShears/NetShears.swift b/Sources/NetShears/NetShears.swift new file mode 100644 index 0000000..935ad7a --- /dev/null +++ b/Sources/NetShears/NetShears.swift @@ -0,0 +1,37 @@ +// +// NetShears.swift +// +// +// Created by Mehdi Mirzaie on 6/4/21. +// + +import Foundation + + +public final class NetShears: NSObject { + + public static let shared = NetShears() + let networkRequestInterceptor = NetworkRequestInterceptor() + var config: NetworkInterceptorConfig = NetworkInterceptorConfig(modifiers: []) + + + public func startRecording(){ + self.networkRequestInterceptor.startRecording() + } + + public func stopRecording(){ + self.networkRequestInterceptor.stopRecording() + } + + public func modify(modifier: RequestEvaluatorModifier) { + config.addModifier(modifier: modifier) + } + + public func modifiedList() -> [RequestEvaluatorModifier] { + return config.modifiers + } + + public func removeModifier(at index: Int){ + return config.removeModifier(at: index) + } +} diff --git a/Sources/NetShears/NetworkInterceptor.swift b/Sources/NetShears/NetworkInterceptor.swift new file mode 100644 index 0000000..853ed71 --- /dev/null +++ b/Sources/NetShears/NetworkInterceptor.swift @@ -0,0 +1,34 @@ +// +// NetworkInterceptor.swift +// +// +// Created by Mehdi Mirzaie on 6/4/21. +// + +import Foundation + + +@objc public class NetworkInterceptor: NSObject { + + @objc public static let shared = NetworkInterceptor() + let networkRequestInterceptor = NetworkRequestInterceptor() + + public func startRecording(){ + self.networkRequestInterceptor.startRecording() + } + + public func stopRecording(){ + self.networkRequestInterceptor.stopRecording() + } + + public func shouldRequestModify(urlRequest: URLRequest) -> Bool { + for modifer in NetShears.shared.config.modifiers { + if modifer.isActionAllowed(urlRequest: urlRequest) { + return true + } + } + return false + } + +} + diff --git a/Sources/NetShears/NetworkInterceptorConfig.swift b/Sources/NetShears/NetworkInterceptorConfig.swift new file mode 100644 index 0000000..9c158ca --- /dev/null +++ b/Sources/NetShears/NetworkInterceptorConfig.swift @@ -0,0 +1,52 @@ +// +// NetworkInterceptorConfig.swift +// +// +// Created by Mehdi Mirzaie on 6/4/21. +// + +import Foundation + +public struct RedirectedRequestModel: Equatable { + public let originalUrl: String + public let redirectUrl: String + + public init (originalUrl: String, redirectUrl: String) { + self.originalUrl = originalUrl + self.redirectUrl = redirectUrl + } +} + +public struct HeaderModifyModel: Equatable { + public let key: String + public let value: String + + public init (key: String, value: String) { + self.key = key + self.value = value + } +} + +public final class NetworkInterceptorConfig { + var modifiers: [RequestEvaluatorModifier] = [] + + init(modifiers: [RequestEvaluatorModifier] = []) { + self.modifiers = modifiers + } + + func addModifier(modifier: RequestEvaluatorModifier) { + self.modifiers.append(modifier) + } + + func getModifiers() -> [RequestEvaluatorModifier] { + return self.modifiers + } + + func removeModifier(at index: Int) { + guard index <= modifiers.count - 1 else { return } + modifiers.remove(at: index) + } + +} + + diff --git a/Sources/NetShears/NetworkRequestInterceptor.swift b/Sources/NetShears/NetworkRequestInterceptor.swift new file mode 100644 index 0000000..443a6f3 --- /dev/null +++ b/Sources/NetShears/NetworkRequestInterceptor.swift @@ -0,0 +1,33 @@ +// +// NetworkRequestInterceptor.swift +// +// +// Created by Mehdi Mirzaie on 6/4/21. +// +import Foundation + + +@objc public class NetworkRequestInterceptor: NSObject{ + + func swizzleProtocolClasses(){ + let instance = URLSessionConfiguration.default + let uRLSessionConfigurationClass: AnyClass = object_getClass(instance)! + + let method1: Method = class_getInstanceMethod(uRLSessionConfigurationClass, #selector(getter: uRLSessionConfigurationClass.protocolClasses))! + let method2: Method = class_getInstanceMethod(URLSessionConfiguration.self, #selector(URLSessionConfiguration.fakeProcotolClasses))! + + method_exchangeImplementations(method1, method2) + } + + public func startRecording() { + URLProtocol.registerClass(NetworkRequestSniffableUrlProtocol.self) + swizzleProtocolClasses() + } + + public func stopRecording() { + URLProtocol.unregisterClass(NetworkRequestSniffableUrlProtocol.self) + swizzleProtocolClasses() + } +} + + diff --git a/Sources/NetShears/RequestModifier/NetShearsModfierProtocol.swift b/Sources/NetShears/RequestModifier/NetShearsModfierProtocol.swift new file mode 100644 index 0000000..3898267 --- /dev/null +++ b/Sources/NetShears/RequestModifier/NetShearsModfierProtocol.swift @@ -0,0 +1,20 @@ +// +// NetShearsModfierProtocol.swift +// +// +// Created by Mehdi Mirzaie on 6/19/21. +// + +import Foundation + +public protocol RequestEvaluator { + func isActionAllowed(urlRequest: URLRequest) -> Bool +} + +public protocol RequestModifier { + func modify(request: inout URLRequest) +} + +public protocol RequestEvaluatorModifier : RequestEvaluator, RequestModifier {} + + diff --git a/Sources/NetShears/RequestModifier/RequestEvaluatorModifierEndpoint.swift b/Sources/NetShears/RequestModifier/RequestEvaluatorModifierEndpoint.swift new file mode 100644 index 0000000..7ff0b3d --- /dev/null +++ b/Sources/NetShears/RequestModifier/RequestEvaluatorModifierEndpoint.swift @@ -0,0 +1,40 @@ +// +// RequestEvaluatorModifierEndpoint.swift +// +// +// Created by Mehdi Mirzaie on 6/19/21. +// + +import Foundation + +public struct RequestEvaluatorModifierEndpoint: RequestEvaluatorModifier, Equatable { + + public var redirectedRequest: RedirectedRequestModel + + public init(redirectedRequest: RedirectedRequestModel) { + self.redirectedRequest = redirectedRequest + } + + public func modify(request: inout URLRequest) { + + if isRequestRedirectable(urlRequest: request) { + request.modifyURLRequestEndpoint(redirectUrl: redirectedRequest) + } + } + + public func isActionAllowed(urlRequest: URLRequest) -> Bool { + return isRequestRedirectable(urlRequest: urlRequest) + } + + func isRequestRedirectable(urlRequest: URLRequest) -> Bool { + guard let urlString = urlRequest.url?.absoluteString else { + return false + } + + if urlString.contains(redirectedRequest.originalUrl) { + return true + } + + return false + } +} diff --git a/Sources/NetShears/RequestModifier/RequestEvaluatorModifierHeader.swift b/Sources/NetShears/RequestModifier/RequestEvaluatorModifierHeader.swift new file mode 100644 index 0000000..715ad1a --- /dev/null +++ b/Sources/NetShears/RequestModifier/RequestEvaluatorModifierHeader.swift @@ -0,0 +1,28 @@ +// +// RequestEvaluatorModifierHeader.swift +// +// +// Created by Mehdi Mirzaie on 6/19/21. +// + +import Foundation + +public struct RequestEvaluatorModifierHeader: RequestEvaluatorModifier, Equatable { + + + public var header: HeaderModifyModel + + public init(header: HeaderModifyModel) { + self.header = header + } + + public func modify(request: inout URLRequest) { + + request.modifyURLRequestHeader(header: header) + } + + public func isActionAllowed(urlRequest: URLRequest) -> Bool { + return true + } + +} diff --git a/Sources/NetShears/URLProtocol/NetworkRequestSniffableUrlProtocol.swift b/Sources/NetShears/URLProtocol/NetworkRequestSniffableUrlProtocol.swift new file mode 100644 index 0000000..2c52ee8 --- /dev/null +++ b/Sources/NetShears/URLProtocol/NetworkRequestSniffableUrlProtocol.swift @@ -0,0 +1,112 @@ +// +// NetworkRequestSniffableUrlProtocol.swift +// +// +// Created by Mehdi Mirzaie on 6/4/21. +// + +import Foundation + +public class NetworkRequestSniffableUrlProtocol: URLProtocol { + static var blacklistedHosts = [String]() + + struct Constants { + static let RequestHandledKey = "NetworkRequestSniffableUrlProtocol" + } + + var session: URLSession? + var sessionTask: URLSessionDataTask? + + 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 public class func canInit(with request: URLRequest) -> Bool { + guard NetworkInterceptor.shared.shouldRequestModify(urlRequest: request) else { return false } + + if NetworkRequestSniffableUrlProtocol.property(forKey: Constants.RequestHandledKey, in: request) != nil { + return false + } + return true + } + + open override class func canonicalRequest(for request: URLRequest) -> URLRequest { + let mutableRequest: NSMutableURLRequest = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest + URLProtocol.setProperty("YES", forKey: "NetworkRequestSniffableUrlProtocol", in: mutableRequest) + return mutableRequest.copy() as! URLRequest + } + + override public func startLoading() { + var newRequest = request + for modifier in NetShears.shared.config.modifiers where modifier.isActionAllowed(urlRequest: request) { + modifier.modify(request: &newRequest) + } + + newRequest.addValue("true", forHTTPHeaderField: "Modified") + sessionTask = session?.dataTask(with: newRequest as URLRequest) + sessionTask?.resume() + } + + override public func stopLoading() { + sessionTask?.cancel() + session?.invalidateAndCancel() + } + + deinit { + session = nil + sessionTask = nil + } +} + +extension NetworkRequestSniffableUrlProtocol: URLSessionDataDelegate { + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + client?.urlProtocol(self, didLoad: data) + } + + public 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) + completionHandler(.allow) + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let error = error { + client?.urlProtocol(self, didFailWithError: error) + } else { + client?.urlProtocolDidFinishLoading(self) + } + } + + public 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) + } + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + guard let error = error else { return } + client?.urlProtocol(self, didFailWithError: error) + } + + public 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 + } + } + } + + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + client?.urlProtocolDidFinishLoading(self) + } +} + diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..2484671 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import NetShearsTests + +var tests = [XCTestCaseEntry]() +tests += NetShearsTests.allTests() +XCTMain(tests) diff --git a/Tests/NetShearsTests/NetShearsTests.swift b/Tests/NetShearsTests/NetShearsTests.swift new file mode 100644 index 0000000..d2b8c53 --- /dev/null +++ b/Tests/NetShearsTests/NetShearsTests.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import NetShears + +final class NetShearsTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + } +} diff --git a/Tests/NetShearsTests/XCTestManifests.swift b/Tests/NetShearsTests/XCTestManifests.swift new file mode 100644 index 0000000..3c11082 --- /dev/null +++ b/Tests/NetShearsTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(NetShearsTests.allTests), + ] +} +#endif diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..c7dab54 Binary files /dev/null and b/logo.png differ