Skip to content

Commit

Permalink
Trusted Entitlements: update handling of 304 responses
Browse files Browse the repository at this point in the history
- `ETagManager` no longer knows anything about signatures
- `HTTPClient` loads cached response from `ETagManager` first before verifying signature with the cached body
- Created new `VerifiedHTTPResponse`, which has the `VerificationResult`, extracted from `HTTPResponse`
- Removed `VerificationResult.from(cache:response:)`
- Updated `MockETagManager` to reflect behavior changed in #2666
- Removed `ETagManager` tests about verification since they're no longer relevant
- Removed several `HTTPClient` tests that were checking a behavior that was impossible (for example, no verification result despite it being enabled), or that checked behavior based on the cached `VerificationResult`
  • Loading branch information
NachoSoto committed Jun 29, 2023
1 parent e654254 commit 73822d9
Show file tree
Hide file tree
Showing 26 changed files with 425 additions and 951 deletions.
59 changes: 14 additions & 45 deletions Sources/Networking/HTTPClient/ETagManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,23 @@ class ETagManager {
static let eTagResponseHeaderName = HTTPClient.ResponseHeader.eTag.rawValue

private let userDefaults: SynchronizedUserDefaults
private let verificationMode: Signing.ResponseVerificationMode

convenience init(verificationMode: Signing.ResponseVerificationMode) {
convenience init() {
self.init(
userDefaults: UserDefaults(suiteName: Self.suiteName)
// This should never return `nil` for this known `suiteName`,
// but `.standard` is a good fallback anyway.
?? UserDefaults.standard,
verificationMode: verificationMode
?? UserDefaults.standard
)
}

init(userDefaults: UserDefaults, verificationMode: Signing.ResponseVerificationMode) {
init(userDefaults: UserDefaults) {
self.userDefaults = .init(userDefaults: userDefaults)
self.verificationMode = verificationMode
}

/// - Parameter withSignatureVerification: whether the request contains a nonce.
func eTagHeader(
for urlRequest: URLRequest,
withSignatureVerification: Bool,
refreshETag: Bool = false
) -> [String: String] {
func eTag() -> (tag: String, date: String?)? {
Expand All @@ -51,28 +47,12 @@ class ETagManager {
return nil
}

let shouldUseETag = (
!withSignatureVerification ||
self.shouldIgnoreVerificationErrors ||
storedETagAndResponse.verificationResult == .verified
)
Logger.verbose(Strings.etag.using_etag(urlRequest,
storedETagAndResponse.eTag,
storedETagAndResponse.validationTime))

if shouldUseETag {
Logger.verbose(Strings.etag.using_etag(urlRequest,
storedETagAndResponse.eTag,
storedETagAndResponse.validationTime))

return (tag: storedETagAndResponse.eTag,
date: storedETagAndResponse.validationTime?.millisecondsSince1970.description)
} else {
Logger.verbose(Strings.etag.not_using_etag(
urlRequest,
storedETagAndResponse.verificationResult,
needsSignatureVerification: withSignatureVerification

))
return nil
}
return (tag: storedETagAndResponse.eTag,
date: storedETagAndResponse.validationTime?.millisecondsSince1970.description)
}

let (etag, date) = eTag() ?? ("", nil)
Expand Down Expand Up @@ -166,13 +146,12 @@ private extension ETagManager {
response: HTTPResponse<Data?>,
eTag: String) {
if let data = response.body {
if response.shouldStore(ignoreVerificationErrors: self.shouldIgnoreVerificationErrors) {
if response.shouldStore() {
self.storeIfPossible(
Response(
eTag: eTag,
statusCode: response.statusCode,
data: data,
verificationResult: response.verificationResult
data: data
),
for: request
)
Expand All @@ -193,10 +172,6 @@ private extension ETagManager {
}
}

var shouldIgnoreVerificationErrors: Bool {
return !self.verificationMode.isEnabled
}

static let suiteNameBase: String = "revenuecat.etags"
static var suiteName: String {
guard let bundleID = Bundle.main.bundleIdentifier else {
Expand All @@ -223,21 +198,17 @@ extension ETagManager {
/// Used by the backend for advanced load shedding techniques.
@DefaultValue<Date?>
var validationTime: Date?
@DefaultValue<VerificationResult>
var verificationResult: VerificationResult

init(
eTag: String,
statusCode: HTTPStatusCode,
data: Data,
validationTime: Date? = nil,
verificationResult: VerificationResult
validationTime: Date? = nil
) {
self.eTag = eTag
self.statusCode = statusCode
self.data = data
self.validationTime = validationTime
self.verificationResult = verificationResult
}

}
Expand All @@ -260,8 +231,7 @@ extension ETagManager.Response {
statusCode: self.statusCode,
responseHeaders: headers,
body: self.data,
requestDate: requestDate,
verificationResult: self.verificationResult
requestDate: requestDate
)
}

Expand All @@ -278,13 +248,12 @@ extension ETagManager.Response {

private extension HTTPResponse {

func shouldStore(ignoreVerificationErrors: Bool) -> Bool {
func shouldStore() -> Bool {
return (
self.statusCode != .notModified &&
// Note that we do want to store 400 responses to help the server
// If the request was wrong, it will also be wrong the next time.
!self.statusCode.isServerError &&
(ignoreVerificationErrors || self.verificationResult != .failed)
!self.statusCode.isServerError
)
}

Expand Down
128 changes: 85 additions & 43 deletions Sources/Networking/HTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class HTTPClient {

typealias RequestHeaders = HTTPRequest.Headers
typealias ResponseHeaders = HTTPResponse<HTTPEmptyResponseBody>.Headers
typealias Completion<Value: HTTPResponseBody> = (HTTPResponse<Value>.Result) -> Void
typealias Completion<Value: HTTPResponseBody> = (VerifiedHTTPResponse<Value>.Result) -> Void

let systemInfo: SystemInfo
let timeout: TimeInterval
Expand Down Expand Up @@ -253,7 +253,7 @@ private extension HTTPClient {
request: Request,
urlRequest: URLRequest,
data: Data?,
error networkError: Error?) -> HTTPResponse<Data>.Result? {
error networkError: Error?) -> VerifiedHTTPResponse<Data>.Result? {
if let networkError = networkError {
return .failure(NetworkError(networkError, dnsChecker: self.dnsChecker))
}
Expand All @@ -262,36 +262,55 @@ private extension HTTPClient {
return .failure(.unexpectedResponse(urlResponse))
}

/// - Returns `nil` if status code is 304, since the response will be empty
/// and fetched from the eTag.
func dataIfAvailable(_ statusCode: HTTPStatusCode) -> Data? {
if statusCode == .notModified {
return nil
} else {
return data
}
}
let statusCode: HTTPStatusCode = .init(rawValue: httpURLResponse.statusCode)

let statusCode = HTTPStatusCode(rawValue: httpURLResponse.statusCode)
// `nil` if status code is 304, since the response will be empty and fetched from the eTag.
let dataIfAvailable = statusCode == .notModified
? nil
: data

return self.createVerifiedResponse(request: request,
urlRequest: urlRequest,
data: dataIfAvailable,
response: httpURLResponse)
}

/// - Returns `Result<VerifiedHTTPResponse<Data>, NetworkError>?`
private func createVerifiedResponse(
request: Request,
urlRequest: URLRequest,
data: Data?,
response httpURLResponse: HTTPURLResponse
) -> VerifiedHTTPResponse<Data>.Result? {
return Result
.success(dataIfAvailable(statusCode))
.mapToResponse(response: httpURLResponse,
request: request.httpRequest,
signing: self.signing(for: request.httpRequest),
verificationMode: request.verificationMode)
.success(data)
.mapToResponse(response: httpURLResponse, request: request.httpRequest)
// Fetch from ETagManager if available
.map { (response) -> HTTPResponse<Data>? in
guard let cachedResponse = self.eTagManager.httpResultFromCacheOrBackend(
return self.eTagManager.httpResultFromCacheOrBackend(
with: response,
request: urlRequest,
retried: request.retried
) else {
return nil
)
}
// Verify response
.map { cachedResponse -> VerifiedHTTPResponse<Data>? in
return cachedResponse?.verify(
request: request.httpRequest,
publicKey: request.verificationMode.publicKey,
signing: self.signing(for: request.httpRequest)
)
}
// Upgrade to error in enforced mode
.flatMap { response -> Result<VerifiedHTTPResponse<Data>?, NetworkError> in
if let response = response,
response.verificationResult == .failed,
case .enforced = request.verificationMode {
return .failure(.signatureVerificationFailed(path: request.httpRequest.path,
code: response.statusCode))
} else {
return .success(response)
}

return cachedResponse
.copy(with: .from(cache: cachedResponse.verificationResult,
response: response.verificationResult))
}
.asOptionalResult?
.convertUnsuccessfulResponseToError()
Expand Down Expand Up @@ -407,7 +426,6 @@ private extension HTTPClient {
if request.httpRequest.path.shouldSendEtag {
let eTagHeader = self.eTagManager.eTagHeader(
for: urlRequest,
withSignatureVerification: request.httpRequest.nonce != nil,
refreshETag: request.retried
)
return request.headers.merging(eTagHeader)
Expand Down Expand Up @@ -499,34 +517,58 @@ private extension NetworkError {

}

extension Result where Success == HTTPResponse<Data>, Failure == NetworkError {

// Converts an unsuccessful response into a `Result.failure`
fileprivate func convertUnsuccessfulResponseToError() -> Self {
return self.flatMap { response in
response.statusCode.isSuccessfulResponse
? .success(response)
: .failure(response.parseUnsuccessfulResponse())
extension Result where Success == Data?, Failure == NetworkError {

/// Converts a `Result<Data?, NetworkError>` into `Result<HTTPResponse<Data?>, NetworkError>`
func mapToResponse(
response: HTTPURLResponse,
request: HTTPRequest
) -> Result<HTTPResponse<Data?>, Failure> {
return self.flatMap { body in
return .success(
.init(
statusCode: .init(rawValue: response.statusCode),
responseHeaders: response.allHeaderFields,
body: body
)
)
}
}

// Parses a `Result<HTTPResponse<Data>>` to `Result<HTTPResponse<Value>>`
func parseResponse<Value: HTTPResponseBody>() -> HTTPResponse<Value>.Result {
return self.flatMap { response in // Convert the `Result` type
Result<HTTPResponse<Value>, Error> { // Create a new `Result<Value>`
try response.mapBody { data in // Convert the body of `HTTPResponse<Data>` from `Data` -> `Value`
try Value.create(with: data) // Decode `Data` into `Value`
}

extension Result where Success == VerifiedHTTPResponse<Data>, Failure == NetworkError {

// Parses a `Result<VerifiedHTTPResponse<Data>>` to `Result<VerifiedHTTPResponse<Value>>`
func parseResponse<Value: HTTPResponseBody>() -> VerifiedHTTPResponse<Value>.Result {
return self.flatMap { response in // Convert the `Result` type
Result<VerifiedHTTPResponse<Value>, Error> { // Create a new `Result<Value>`
try response.mapBody { data in // Convert the from `Data` -> `Value`
try Value.create(with: data) // Decode `Data` into `Value`
}
.copyWithNewRequestDate() // Update request date for 304 responses
.copyWithNewRequestDate() // Update request date for 304 responses
}
// Convert decoding errors into `NetworkError.decoding`
.mapError { NetworkError.decoding($0, response.body) }
.mapError { NetworkError.decoding($0, response.response.body) }
}
}

}

extension Result where Success == VerifiedHTTPResponse<Data>, Failure == NetworkError {

// Converts an unsuccessful response into a `Result.failure`
fileprivate func convertUnsuccessfulResponseToError() -> Self {
return self.flatMap {
$0.response.statusCode.isSuccessfulResponse
? .success($0)
: .failure($0.response.parseUnsuccessfulResponse())
}
}

}

private extension HTTPResponse {
private extension VerifiedHTTPResponse {

func copyWithNewRequestDate() -> Self {
// Update request time from server unless it failed verification.
Expand All @@ -538,7 +580,7 @@ private extension HTTPResponse {
}

var isLoadShedder: Bool {
return self.value(forHeaderField: HTTPClient.ResponseHeader.isLoadShedder.rawValue) == "true"
return self.response.value(forHeaderField: HTTPClient.ResponseHeader.isLoadShedder.rawValue) == "true"
}

}
Expand Down
Loading

0 comments on commit 73822d9

Please sign in to comment.