Implementation of Networking client and Mock Networking client.
To use Networking in a SwiftPM project:
- Add the following line to the dependencies in your
Package.swift
file:
.package(url: "https://github.com/govuk-one-login/mobile-ios-networking", from: "1.0.0"),
- Add
Networking
as a dependency for your target:
.target(name: "MyTarget", dependencies: [
.product(name: "Networking", package: "dcmaw-networking")
]),
Or for MockNetworking
:
.testTarget(name: "MyTestTarget",
dependencies: [
"MyTarget",
.product(name: "MockNetworking", package: "Networking")
]
)
- Add
import Networking
in your source code.
The main Networking
Package is a unified implementation for networking to ensure that all HTTP requests made from the app are consistently well-formed. It also ensures that requests are pinned (to Amazon Root Certificates) and a user agent is attached.
Certificate pinning is not recommended in general for mobile applications, given the sensitive nature of our applications, we have decided to add this as an additional layer of protection against man-in-the-middle attacks.
The iOS validation would need to be set-up in your app codebase Info.plist for devices which are running iOS 14 or later. The Apple Developer documentation explains how this is set up. There is no additional setup for devices running iOS 13 or lower, as we have included that in this package. //add link to documentation
Within Sources/Networking exist the following protocols and Type for enabling the app to make network requests and pin certificates.
AppBundle
used to extend Bundle
to provide an abstraction layer to decouple required properties for use in UserAgent
struct and in mocks.
Device
used to extend Bundle
to provide an abstraction layer to decouple required properties for use in UserAgent
struct and in mocks.
SecurityEvaluator
for evaluating server trust and getting certificates
NetworkClient
is a class with two public async throwing methods called makeRequest
and makeAuthorizedRequest
which handle network requests and return Data
.
NetworkClient
is initialised with a URLSessionConfiguration
. It has a convenience init
that initialises configuration
with the .ephemeral
singleton on URLSessionConfiguration
which avoids needing to provide one at initialisation.
For iOS 14 and later, certificates are pinned using NSAppTransportSecurity
. Earlier versions of iOS use SSLPinningDelegate
which conforms to URLSessionDelegate
protocol to handle certificate pinning.
The signature of the makeRequest
method is:
public func makeRequest(_ request: URLRequest) async throws -> Data
The signature of the makeAuthorizedRequest
method is:
public func makeAuthorizedRequest(exchangeRequest: URLRequest,
scope: String,
request: URLRequest) async throws -> Data
Both of these methods handle various response types and return or throw errors as appropriate.
The URLSessionConfiguration.tlsMinimumSupportedProtocolVersion
is then set to .TLSv12
and .httpAdditionalHeaders
is set like so:
configuration.httpAdditionalHeaders = ["User-Agent": UserAgent().description]
SSLPinningDelegate
class is used for handling certificate pinning in iOS 13 and earlier. In the NetworkClient
initialiser it is called conditionally.
Conforms to: NSObject
, URLSessionDelegate
X509CertificateSecurityEvaluator
concrete implementation of SecurityEvaulator
Conforms to: SecurityEvaluator
UserAgent
is a struct encapsulating helpful additional information to be included in HTTP headers when making network requests.
This enables us to see in our backend logs with version of the app is making the call, making it easier for us to triage issues and fix bugs.
Conforms to: CustomStringConvertible
You can set various details in the UserAgent, line in the below samples. It will also set the app name from CFBundleName
and the version from CFBundleShortVersionString
. We pass these through to a description
element and then use that with the network call. For example, for GOV.UK ID Check 1.18.0 running on iPhone SE 2 on iOS 16.5.1, this would show up as ID_Check/1.18.0 iPhone12,8 iOS/16.5.1 CFNetwork/1408.0.4 Darwin/22.5.0
private var appInfo: String {
"\(appName)/\(appVersion)"
}
var description: String {
"\(appInfo) \(deviceModel) \(osVersion) \(cfNetwork) \(darwin)"
}
DarwinVersion
used as part of assembling the UserAgent
properties. This type gets the Darwin
version of the current OS from the utsname
struct.
Conforms to: CustomStringConvertible
DeviceModel
used as part of assembling the UserAgent
properties. This type gets the device model from the utsname
struct.
Conforms to: CustomStringConvertible
Version
used as part of assembling the UserAgent
properties. It has two initialisers. The one used when creating the UserAgent
takes a String
argument and separates it into components for major
, minor
and increment
version numbers which are then stored in constant properties. The other initialiser accepts separate Int
types directly for major
, minor
and increment
.
Conforms to: CustomStringConvertible
, Decodable
and Comparable
.
Extension on HTTPURLResponse
adds an isSuccessful
bool computed property that based on the statusCode
of the response. For statusCode
in the range 200 to 299 inclusive it returns true
, otherwise it returns false
. This allows querying HTTP response codes directly on the HTTP response for example:
guard response.isSuccessful else {
// do work if unsuccessful
return
}
// do work if successful
ErrorWithCode
conforms to Error
and includes the following requirements:
var hash: String? { get }
var errorCode: Int { get }
var endpoint: String? { get }
errorCode
being the HTTP error code from a network request and endpoint
can be set when initialising the error.
There is a protocol extension on ErrorWithCode
that returns and stores a hash from the errorCode
and endpoint
properties. This hash uses a deterministic hashing algorithm, as such, you can rely on the output hash being the same for consistent input values. This makes the hash
property useful for error tracking and reporting via logging or analytics.
ServerError
conforms to ErrorWithCode
. It includes a constant string property reason
which is set to "server"
There is then an extension on ServerError
which adds a parameters
dictionary computed property of type [String: String]. This includes the
endpoint,
code(the
errorcode),
hashand
reason` properties. This is for analytics and logging purposes, allowing a single property to be submitted to a remote service if required.
To use the NetworkClient
first make sure your module or app has a dependency on Networking
and the file has an import for Networking
. Then initialise an instance of NetworkClient
and create a URLRequest. Then make the network request using the makeRequest
method.
import Networking
...
let client = NetworkClient() // initialised with URLSessionConfiguration.ephemeral
...
let requestURL = baseURL.appendingPathComponent("someURLPath")
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
do {
let data = try await client.makeRequest(request)
// decode data
} catch {
// handle errors
}
How you handle the returned data would depend on what data you expect to be returned.