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