diff --git a/Docs/CertificatePinning.md b/Docs/CertificatePinning.md new file mode 100644 index 0000000..c55f868 --- /dev/null +++ b/Docs/CertificatePinning.md @@ -0,0 +1,106 @@ +## **iOS Security: Certificate Pinning** +As developers we need our apps to: work properly (obviously), be easy to mantain, testable, scalable and **SECURE**. +Users will trust their confidential data to our apps and we need to handle that carefully and prevent leaks or flaws that can cause data loss or exposure. + +I always recommend to follow OWASP and their Mobile Apps Top 10 security threats and the solutions for it +(https://owasp.org/www-project-mobile-top-10/) + +The threat that is treated by QuickHatch is Top 3 Insecure Communication (https://owasp.org/www-project-mobile-top-10/2016-risks/m3-insecure-communication). + +The Certificate Pinning Module is made to prevent Man in the middle attacks (you can find more detailed information in the OWASP links) but basically what we are preventing is that an attacker is able to see all your network traffic. + +Certificate Pinning works on top of HTTPS requests and what is gonna do is to verify the authenticity of the SSL Certificate used by the server and the connection between your device and server. + +For more information about TLS/SSL handshake and how it works check OWASP links. +https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning + +The module has 2 strategies to validate the certificate: Pin the certificate and pin the public key. +which one to choose ? Its up to you, if you choose to pin the certificate you will have to update the app bundle every time the certificate is updated, however if you pin the public key you wont have this problem because the public key is not going to change, but you may violate the key rotation standard (its always good to change the keys of your app). +Lets jump right to the Code: + +```swift + public protocol CertificatePinner { + func isServerTrusted(challenge: URLAuthenticationChallenge) -> Bool + } +``` + +First of all we have a protocol CertificatePinner with only one function that is to check wether the server we are receiving is trusted by our app or not. **URLAuthenticationChallenge** is the swift class that contains information about the server certficate. +```swift +public class QHCertificatePinner: CertificatePinner { + private let pinningStrategies: [String: [PinningStrategy]] + + public init(pinningStrategies: [String: [PinningStrategy]]) { + self.pinningStrategies = pinningStrategies + } + .... + ... +``` + +Here is the QuickHatch implementation: you initialize your pinner with a dictionary of hosts (string) with pinning strategies for those hosts. + +The pinning strategy protocol: +```swift +public protocol PinningStrategy { + func validate(serverTrust: SecTrust) -> Bool +} +``` + +CertificatePinning strategy: +```swift +public class CertificatePinningStrategy: PinningStrategy { + private let certificates: [SecCertificate] + + public init(certificates: [SecCertificate]) +``` +its initialized with an array of Certificates (included in your bundle) and the validate function will use those certificates. + +The public key strategy: +```swift +public class PublicKeyPinningStrategy: PinningStrategy { + private let publicKeys: [String] + private let hasher: Hasher + + public init(publicKeys: [String], hasher: Hasher = CCSHA256Hasher.shared) +``` +This implementation is initialized with an array of public keys (hash) and with a hasher class that is gonna apply the hashing to the public key coming in the **URLAuthenticationChallenge** and is gonna compare 2 strings to validate. + + +Real Example: +```swift + +let pinner = QHCertificatePinner(pinningStrategies: ["quickhatch.com": [CertificatePinningStrategy(certificates: [certificate], + PublicKeyPinningStrategy(publicKeys: ["your public key has"], + hasher: youHasher)) + ] + ]) +``` + +In this case we set 2 pinning strategies for **quickhatch.com** one public key pinning with one public key and your own hasher implementation and one Certificate pinning strategy with one certificate. + +By default if the host doesnt have a pinning strategy set it will return as trusted. + +All of this look great and fancy but where do we use our pinner ??? + +The answer is URLSession method **urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)** so There is a class implementing URLSessionDelegate, you can use this class to initialize your urlsession or you can subclass it you choice. +```swift +public class URLSessionPinningDelegate: NSObject, URLSessionDelegate { + private let certificatePinner: CertificatePinner + + public init(certificatePinner: CertificatePinner) { + self.certificatePinner = certificatePinner + } + + public func urlSession(_ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + if certificatePinner.isServerTrusted(challenge: challenge) { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + return + } + completionHandler(.cancelAuthenticationChallenge, nil) + } +``` \ No newline at end of file diff --git a/Docs/CodableExtensions.md b/Docs/CodableExtensions.md new file mode 100644 index 0000000..9c14121 --- /dev/null +++ b/Docs/CodableExtensions.md @@ -0,0 +1,26 @@ +## **Using Codable extension** + +Codable (Encodable & Decodable) is a protocol launched by Apple to encode and decode JSONs very easily, +QuickHatch provides an extension for mapping responses to an object or an array. + +This is a sample for a **response** mapping using QuickHatchHTTP: + +```swift + struct User: Codable { + var name: String + var age: Int + } + + let request = networkRequestFactory.response(request: yourRequest) { (result: Result, Error>) in + switch result { + case .success(let response): + //do something + case .failure(let error): + //handle error + } + } + request.resume() +``` + + +--- diff --git a/Docs/CombineSupport.md b/Docs/CombineSupport.md new file mode 100644 index 0000000..ce1bfcc --- /dev/null +++ b/Docs/CombineSupport.md @@ -0,0 +1,27 @@ +## **Using Combine extension** + +The network Request Factory also has support for swift Combine if you decide to go full Reactive programming. + +```swift + func response(urlRequest: URLRequest, + jsonDecoder: JSONDecoder = JSONDecoder()) -> AnyPublisher, Error> +``` +This is a sample for a **response** mapping using QuickHatch Combine function: + +```swift + struct User: Codable { + var name: String + var age: Int + } + var subscriptions: Set = [] + + let request = networkRequestFactory.response(request: yourRequest) + .sink(receiveCompletion: { receiveCompletion in + + }, + receiveValue: { (value: User) in + }).store(in: &subscriptions) +``` + + +--- diff --git a/Docs/Error.md b/Docs/Error.md new file mode 100644 index 0000000..6ac3313 --- /dev/null +++ b/Docs/Error.md @@ -0,0 +1,47 @@ +## **Errors** +- If an error is returned in a network request QuickHatchHTTP provides an enum with errors +```swift +public enum RequestError: Error, Equatable { + case unauthorized + case unknownError(statusCode: Int) + case cancelled + case noResponse + case requestWithError(statusCode: HTTPStatusCode) + case serializationError(error: Error) + case invalidParameters + case malformedRequest + case other(error: Error) +} +``` +Now if we want to check what error is.. +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private weak var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.data(urlRequest: request) { result in + switch result { + case .success(let response): + // show error messsage + view?.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + if let requestError = error as? RequestError, + requestError == .unauthorized { + //show auth error + } + view?.showErrorMessage("Error downloading profile image") + } + } +} +``` +In this sample we are checking whether the error is unauthorized type or not. diff --git a/Docs/GettingStarted.md b/Docs/GettingStarted.md new file mode 100644 index 0000000..81a52dc --- /dev/null +++ b/Docs/GettingStarted.md @@ -0,0 +1,118 @@ +## **Getting Started** + +- When you develop an app usually you have to fetch data from server/s. +- due to this you will want to use a Networking framework but you want to keep the code clean, independent and mantainable. +- You may want to use libraries like Alamofire, Malibu or your own implementation using URLSession or URLConnection. +- What QuickHatch provides is a set of protocols that allows you to follow clean code standards, SOLID principles and allows you to be flexible when it comes to Networking. +- First of all, your app will use a NetworkClient (this can be Alamofire, etc), and QuickHatch provides a protocol called NetworkRequestFactory that sets what a network client should respond when making a request. + +![](https://github.com/dkoster95/QuickHatchHTTP/blob/main/diagram.png) + +The name of this networkLayer interface in QuickHatchHTTP is NetworkRequestFactory :). +```swift + +public protocol NetworkRequestFactory { + func data(request: URLRequest, + dispatchQueue: DispatchQueue, + completionHandler completion: @Sendable @escaping (Result) -> Void) -> DataTask + func dataPublisher(request: URLRequest) -> AnyPublisher + func data(request: URLRequest) async throws -> HTTPResponse +} +``` + +The very base protocol of the RequestFactory provides 2 functions to get Data out of a network request, one is using a ***completionHandler*** with a ***Request*** type return and the other one is using ***Combine's AnyPublisher*** if you choose to use Reactive Programming + +Now lets say your app is using a MVP (Model-View-Presenter) pattern and you want to use QuickHatch to map your networking layer. +Lets show a sample of how that would be: + +First we have our presenter, class in charge of connecting View and Models. +Keep in mind these its just a sample, you should always use clean code principles such as SOLID or KISS. + + +```swift + class ViewPresenter: Presenter { + private var requestFactory: NetworkRequestFactory + var view: View? + + init(requestFactory: NetworkRequestFactory) { + self.requestFactory = requestFactory + } + + func fetchData(username: String) { + view?.showLoading() + // Here you initialize the URLRequest object, for now we will use a quickHatch get request + let yourRequest = URLRequest.get(url: URL("www.google.com", + parameters: ["username": username], + parameterEncoding: URLEncoding.queryString) + requestFactory.data(request: yourRequest, dispatch: .main) { result in + view?.stopLoading() + switch result { + case .success(let data): + view?.showSuccessAlert(data) + case .failure(let error): + view?.showErrorAlert(error) + } + + }.resume() + } + } +``` +Here we have a View type (protocol), a fetchData method that is going to use the NetworkFactory to get the data from somewhere using a data response and we have the initializer where we are injecting the networkFactory. +Now previously if we used some NetworkClient framework here we would be using the implementation class instead of the protocol and we would be attached to that framework, +but with QuickHatch you are attaching yourself to a protocol, and the implementation can change anytime. + +And now we have the code of our view and the dependency injector: + +```swift + struct DependencyInjector { + func initializeSampleView() { + let presenter = ViewPresenter(requestFactory: QHRequestFactory(urlSession: URLSession.shared) + let sampleView = SampleView(presenter: presenter) + application.rootView = sampleView + } + } +``` + +```swift + class SampleView: View { + + private var presenter: ViewPresenter + private var textField: TextField + + init(presenter: Presenter) { + self.presenter = presenter + self.presenter.view = self + } + + @IBAction buttonTapped() { + presenter.fetchData(textField.text) + } + + func stopLoading() { + // stop spinner + } + + func showLoading() { + // start spinner + } + + func showSuccessAlert(data: Data) { + // show alert for success case + } + + func showErrorAlert(error: Error) { + // show alert for error case + } + } +``` + +Great! now we have a presenter that uses an abstract networking layer, and we can switch the implementation by changing the code in the dependencyInjector, +in this case we used the NetworkFactory implementation that QuickHatch provides, this one is initizalized with a URLSession object. +If we want to use an Alamofire Implementation: +you go to the dependency injector and set the new implementation. + +```swift + let presenter = ViewPresenter(requestFactory: AlamofireRequestFactory()) +``` + +--- diff --git a/Docs/JSONEncoding.md b/Docs/JSONEncoding.md new file mode 100644 index 0000000..20259b7 --- /dev/null +++ b/Docs/JSONEncoding.md @@ -0,0 +1,31 @@ +## **JSON Parameter Encoding** +- Another important feature of the framework is the **parameter encoding** for a URLRequest + + +Here you see the parameter encoding protocol, simple right ? +the framework provides 3 implementations for this protocol URL, JSON and String encodings but you can create your own implementation and then inject it into the URLRequest initializer. + +```swift +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} +``` + +The JSON encoding will escape and encode the parameters in the body. + +It will look like this +params: name and password +```swift +{ + "name": "quickhatch", + "password": "1234" +} +``` + +Real Sample: +```swift +let getUsers = URLRequest.post(url: URL("getusers.com"), + params: ["page":1], + encoding: JSONEncoding.default, + headers: ["Authorization": "token 1232"]) +``` \ No newline at end of file diff --git a/Docs/Request-Response.md b/Docs/Request-Response.md new file mode 100644 index 0000000..0c09603 --- /dev/null +++ b/Docs/Request-Response.md @@ -0,0 +1,41 @@ +## **Request/Response Pattern** +- QuickHatchHTTP provides an easy way to create a request and fetch the response like Python Requests does +```swift +public protocol HTTPRequest { + var headers: [String: String] { get } + var body: Data? { get } + var url: String { get } + var method: HTTPMethod { get } +} + +public protocol HTTPRequestActionable { + func response() async throws -> HTTPResponse + var responsePublisher: any Publisher { get } +} + +public protocol HTTPRequestDecodedActionable { + associatedtype ResponseType: Codable + func responseDecoded() async throws -> Response + var responseDecodedPublisher: any Publisher { get } +} + +public protocol HTTPResponse { + var statusCode: HTTPStatusCode { get } + var headers: [AnyHashable: Any] { get } + var body: Data? { get } +} +``` + +Now Some samples... + + +```swift +let getUsersRequest = QHHTTPRequest(url: "quickhatch", method: .get, headers: ["Authorization": "token1234"]) +do { +let response = try await getUsers.responseDecoded() +} catch let error as RequestError { + //do something with error +} +``` + +Building a request is so easy now! diff --git a/Docs/Responses.md b/Docs/Responses.md new file mode 100644 index 0000000..ae7b3f3 --- /dev/null +++ b/Docs/Responses.md @@ -0,0 +1,70 @@ +## **Data responses** +- Use QuickHatchHTTP to parse your request responses into the standard types +- Handle the response however you want to do it + +Remember the Base protocol.... +```swift +public protocol NetworkRequestFactory { + func data(request: URLRequest, + dispatchQueue: DispatchQueue, + completionHandler completion: @Sendable @escaping (Result) -> Void) -> DataTask + func data(request: URLRequest) async throws -> HTTPResponse +} + +``` +If we want to handle our login response back as **Data** Type.... + +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.data(request: request) { result in + switch result { + case .success(let response): + // show error messsage + view.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + view.showErrorMessage("Error downloading profile image") + } + } +} +``` + +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.data(request: request) { result in + switch result { + case .success(let response): + // show error messsage + view.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + view.showErrorMessage("Error downloading profile image") + } + } +} +``` +This is an alternative for handling the responses, you can also check the Codable Extension to parse the response into a Codable Object of your own diff --git a/Docs/StringEncoding.md b/Docs/StringEncoding.md new file mode 100644 index 0000000..e8e1f7e --- /dev/null +++ b/Docs/StringEncoding.md @@ -0,0 +1,33 @@ +## **String Parameter Encoding** +- Another important feature of the framework is the **parameter encoding** for a URLRequest + + +Here you see the parameter encoding protocol, simple right ? +the framework provides 3 implementations for this protocol URL, JSON and String encodings but you can create your own implementation and then inject it into the URLRequest initializer. + +```swift +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} +``` + +The String encoding will escape and encode the parameters in the body or the URL, you can configure that. + +It will look like this if you choose the body: +params: name and password +```swift +httpBody = "quickhatch&1234" +``` + +But if you choose the URL as destination: +There is an interesting mapping provided by the Encoder +you have page and max(per page) as parameters +```swift + let getUsers = URLRequest.get(url: URL("getusers.com/{page}/{max}"), + params: ["page":1,"maxperpage":30], + encoding: StringEncoding.urlEncoding, + headers: ["Authorization": "token 1232"]) + yourURL will be "getusers.com/1/30" +``` + + diff --git a/Docs/URLEncoding.md b/Docs/URLEncoding.md new file mode 100644 index 0000000..b486417 --- /dev/null +++ b/Docs/URLEncoding.md @@ -0,0 +1,33 @@ +## **Url Parameter Encoding** +- Another important feature of the framework is the **parameter encoding** for a URLRequest. + + +Here you see the parameter encoding protocol, simple right ? +the framework provides 3 implementations for this protocol: URL, JSON and String encoders but you can create your own implementation and then inject it into the URLRequest initializer. + +```swift +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} +``` + +The **Url encoding** will escape and encode the parameters in the url of the request or in the body depending the destination you choose. + +If you choose the body for a post request the property httpBody of the URLRequest will look like this: +params: name and password +```swift +"name=quickhatch&password=1232" +``` + +If you choose the URL it will look like this: +```swift +"www.theURL.com/api?name=quickhatch&password=1232" +``` + +Real Sample: +```swift +let getUsers = URLRequest.post(url: URL("getusers.com"), + params: ["page":1], + encoding: URLEncoding.bodyEncoding, + headers: ["Authorization": "token 1232"]) +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d12d40 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ + + +# QuickHatchHTTP + +[![Swift](https://img.shields.io/badge/Swift-6.0-green)](https://img.shields.io/badge/Swift-6.0-green) +[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_visionOS_Linux_Windows_Android-green)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_vision_OS_Linux_Windows_Android-Green) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-green)](https://img.shields.io/badge/Swift_Package_Manager-compatible-green) +- **Use an abstract networking layer** +- **Request/Response Pattern** +- **Keep networking simple** +- **Swift Concurrency support** +- **Combine support** +--- + + +## Features + +- **NetworkRequestFactory protocol**: + - [Getting started](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/GettingStarted.md) + - [Codable extension](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/CodableExtensions.md) + - [Data, String responses](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/Responses.md) + - [Combine Support](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/CombineSupport.md) + - [Errors](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/Error.md) +- **URLRequest Additions**: + - [HTTP Methods oriented](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/URLRequestExtension.md) +- **Parameter Encoding** + - [URLEncoding](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/URLEncoding.md) + - [JSONEncoding](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/JSONEncoding.md) + - [StringEncoding](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/StringEncoding.md) +- **Request/Response Pattern** + - [Index](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/HostEnvironment.md) +- **Certificate Pinning** + - [Index](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/CertificatePinning.md) +--- + +## Requirements + +- iOS 15.0+ +- WatchOS 7.0+ +- TvOS 14.0+ +- MacOS 12.14+ +- Xcode 10.2+ +- Swift 6+ + +--- + +## Installation + +## Swift Package Manager +QuickHatch has support for SPM, you just need to go to Xcode in the menu File/Swift Packages/Add package dependency +and you select the version of QuickHatchHTTP. + diff --git a/diagram.png b/diagram.png new file mode 100644 index 0000000..e799bec Binary files /dev/null and b/diagram.png differ