GithubHelp home page GithubHelp logo

icapps / ios-cara Goto Github PK

View Code? Open in Web Editor NEW
1.0 5.0 4.0 31 MB

Our generic webservice layer

License: MIT License

Ruby 5.00% Swift 94.71% Objective-C 0.29%
networking serializer webservice-layer xcode certificate-pinning public-key-pinning cocoapods carthage cara swift-package-manager request response ios macos tvos concurrency async-await swift

ios-cara's Introduction

[CI Status Language Swift 4.0

Cara is the webservice layer that is (or should be) most commonly used throughout our apps.

TOC

Installation ๐Ÿ’พ

Swift Package Manager

You can install Cara using the Swift Package Manager. This is available starting from Xcode 11. Just search for icapps/ios-cara and install it.

Cocoapods

Cara is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Cara', git: 'https://github.com/icapps/ios-cara.git', commit: '...'

Pass the correct commit reference to make sure your code doesn't break in future updates.

Features

Configuration

In order to use the service layer you have to configure it. This can be done by implementing the Configuration protocol and passing it to the Service init function.

let configuration: Configuration = SomeConfiguration()
let service = Service(configuration: configuration)

Once this is done you are good to go. For more information on what configuration options are available, take a look at the documentation inside the Configuration.swift file.

Interceptor

An intercept will intercept the request when an error of type ResponseError occurs.

When this happens intercept(_:data:retry:) will be triggered and you should return true or false to indicate if you want the normal response flow to stop. When you stop the flow it will be possible to retry the request by calling the retry() block.

func intercept(_ error: ResponseError, data: Data?, retry: @escaping () -> Void) -> Bool {
    if error == .unauthorized {
        // Execute the retry block after one second. The failed request will be retried.
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: retry)
        return true
    } else if error == .serviceUnavailable {
        // When this error occurs the flow will be stopped. So the normal completion block after serialization will not be 
        // triggered.
        return true
    } else {
        // Continue the flow and start serializing.
        return false
    }
}

โš ๏ธ The Request can define it it can be intercepted with the isInterceptable property. This should always be true in most cases. But, for example, the refresh tokens request should not be intercepted.

Trigger a Request

In order to trigger a request you have to do 2 things:

  • Create a request that conforms to Request.

    The request configuration will be done in this instance. For more information on what options are available, take a look at the documentation inside the Request.swift file.

  • Create a serializer that conforms to Serializer.

    The serialization of the response will be done here. You have to implement the serialize(data:error:response:) function and this will be called when the response completes.

Once both instances are created and you Service is configured, you can execute the request.

let request: Request = SomeRequest()
let serializer: Serializer = JSONSerializer()
let task = service.execute(request, with: serializer) { response in
    ...
}

The response returned by the completion block is the same as result of the serializer's serialize(data:error:response:) function. Executing a request returns a URLSessionDataTask, this can be used to, for example, cancel the request.

When you trigger a request on a custom queue, the execution block will return on that same queue. This way you have full control of the threading in your application.

Serialization

With every request execution you have to pass a serializer. In most cases you will be able to use our CodableSerializer, but when you want to define a custom way of serializing your data, there is room for that too.

Custom Serializer

Create a custom class that conforms to Serializer. Here is a small example of how to do this.

struct CustomSerializer: Serializer {
    enum Response {
        case .success
        case .failure(Error)
    }

    func serialize(data: Data?, error: Error?, response: HTTPURLResponse?) -> Response {
        // data: data returned from the service request
        // error: error returned from the service request
        // response: the service request response

        if let error = error {
            return .failure(error)
        } else {
            return .success
        }
    }
}

Codable Serializer

We aleady supplied our Cara framework with one serializer: the CodableSerializer.

This serializer can parse the json data returned from the service to your codable models. Here is an example of a simple Codable model:

class User: Codable {
    let name: String
}

Let's now see how we can serialize the result of a request to a single User model:

let request = SomeRequest()
let serializer = CodableSerializer<User>()
service.execute(request, with: serializer) { response in
    switch response {
    case .success(let model):
        // The `model` instance is the parsed user model.
    case .failure(let error):
        // The `error` instance is the error returned from the service request.
    }
    ...
}

But what if multiple models are returned? Easy:

let request = SomeRequest()
let serializer = CodableSerializer<[User]>()
service.execute(request, with: serializer) { response in
    switch response {
    case .success(let models):
        // The `models` array contains the parsed `User` models.
    case .failure(let error):
        // The `error` instance is the error returned from the service request.
    }
    ...
}

When required you can pass a custom JSONDecoder through the init.

Logger

You can get some information about the request and it's response. This can come in handy when you want to log all the request to the Console. In order to get what you want to have to create an object that conforms to Logger and pass it to Cara through the Configuration.

Custom Logger

Create a custom class that conforms to Logger. Here is a small example of how to do this.

struct CustomLogger: Logger {
    func start(urlRequest: URLRequest) {
        // Triggered just before a request if fired
    }

    func end(urlRequest: URLRequest, urlResponse: URLResponse, metrics: URLSessionTaskMetrics, error: Error?) {
        // Triggered just after the request finised collecting the metrics
    }
}

When you want to use the CustomLogger in your application you have to pass it to the loggers array in the configuration.

class SomeConfiguration: Configuration {
    ...

    var loggers: [Logger]? {
        return [CustomLogger()]
    }
}

Console Logger

We aleady supplied our Cara framework with one logger: the ConsoleLogger.

This logger send the request and response information through os_log to the console. Below is an example of the printed logs:

When you want to use the ConsoleLogger in your application you have to pass it to the loggers array in the configuration.

class SomeConfiguration: Configuration {
    ...

    var loggers: [Logger]? {
        return [ConsoleLogger()]
    }
}

Public Key Pinning

You can also make sure that some URL's are pinned for security reasons. It's fairly simple on how you can do this. Just add the correct host with it's SHA256 encryped public key to the publicKeys property of the Configuration.

class SomeConfiguration: Configuration {
    ...

    var publicKeys: PublicKeys? {
        return [
            "apple.com": "9GzkflclMUOxhMgy32AWL/OGkMZF/5NIjvL8M/4rb3k=",
            "google.com": "l2Z/zhy2hByKIqvgRkpKRm6M234/2HAEwiPXx5T8YYI="
        ]
    }
}

There is a quick way to get the correct public key for a certain domain. Go to SSL Server Test by SSL Labs in order to perform an analysis of the SSL configuration of any web server. In the inputfield you enter the domain in order to get the process started. On the next page click the first IP address that appears, and on the page after, you'll notice the Pin SHA256 field. The value is the public key string we need.

Contribute

How to contribute โ“

  1. Add a Github issue describing the missing functionality or bug.
  2. Implement the changes according to the Swiftlint coding guidelines.
  3. Make sure your changes don't break the current version. (deprecate is needed)
  4. Fully test the added changes.
  5. Send a pull-request.

Contributors ๐Ÿค™

License

Cara is available under the MIT license. See the LICENSE file for more info.

ios-cara's People

Contributors

apps2create avatar dgyesbreghs avatar fousa avatar herre avatar icappscedric avatar ikbendewilliam avatar ronaldhollanderica avatar vanlooverenkoen avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

ios-cara's Issues

Handle logout

When the token refresh fails we should be able to tell the client that a logout occurred.

Maybe it's a good idea to make it possible to handle a response in a different way then triggering the default completion handler. ex. HTTP status code 503 should trigger something. This way we could also handle the token refresh #14

Cache the public keys once generated

The public keys are generated for the domain on every request. but I think we should cache the generated value so that the public key pinning process is optimised.

Support Codable types in request body

Currently Cara supports two types for the body parameter: Data and any Foundation type that can be serialised using JSONSerialization.

It would be nice if it would also support a third type: Codable. That would allow us to use any type that implements the Codable protocol as the body.

Currently we are working around this by manually encoding to Data and then using that in the body, but that makes error handling cumbersome and is error prone to do every time.

Make it possible to use a background `URLSessionConfiguration`

This is useful when you want to send analytics to your own server, and you want this to happen in the most performant way.

Make sure that the completion handler can be triggered from the UIApplicationDelegate.

func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void)

Handle token refresh

We need a mechanism where a refresh can be executed when some HTTP status code is thrown. The refresh will an another Request.

Make sure that when we fire a lot of requests at the same time that the refresh Request isn't triggered multiple times. So some blocking the thread and keeping track of the triggered requests should be handled.

URLAuthenticationChallenge always canceled when authentication method is not "server trust"

I am by no means an expert on authentication, this is just what I found by trial and error.

When making a request, sometimes an authentication challenge of type NSURLAuthenticationMethodClientCertificate is received. This causes Cara to cancel the authentication challenge, and therefore failing the network request.

Would it be possible to change this behaviour? I tried it with completionHandler(.performDefaultHandling, nil), and that seems to work fine. I have no idea what the security implications of that are though.

This happens on NetworkService.swift:88:

guard
    let serverTrust = challenge.protectionSpace.serverTrust,
    challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
    completionHandler(.cancelAuthenticationChallenge, nil) // <---
    return
}

Add an example project

The example project should contain:

  • Different request
  • Different serializers
  • Reachability
  • Different loggers

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.