-
Notifications
You must be signed in to change notification settings - Fork 91
feat: Automatic clock skew adjustment #2023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
dc93edd
71ea35f
3459208
db78779
dafec4c
b0c4b1e
336919b
3faee78
465103d
620b246
f265647
9cf8f71
7568ac7
0436058
8c9d68f
cd2afd7
2114730
f0b716f
6227b8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -574,6 +574,7 @@ private var runtimeTargets: [Target] { | |
.SmithyIdentity, | ||
.SmithyRetriesAPI, | ||
.SmithyRetries, | ||
.SmithyTimestamps, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See |
||
.AWSSDKCommon, | ||
.AWSSDKHTTPAuth, | ||
.AWSSDKChecksums, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import struct Foundation.Date | ||
import struct Foundation.TimeInterval | ||
import class SmithyHTTPAPI.HTTPRequest | ||
import class SmithyHTTPAPI.HTTPResponse | ||
@_spi(SmithyTimestamps) import struct SmithyTimestamps.TimestampFormatter | ||
|
||
public enum AWSClockSkewProvider { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the AWS-specific clock skew provider. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No further explanation, see inline code comments. |
||
// Any clock skew less than this threshold will not be compensated. | ||
private static var clockSkewThreshold: TimeInterval { 300.0 } // five minutes | ||
|
||
@Sendable | ||
public static func clockSkew( | ||
request: HTTPRequest, | ||
response: HTTPResponse, | ||
error: Error, now: Date | ||
) -> TimeInterval? { | ||
// Need a server Date and an error code to calculate clock skew. | ||
// If either of these aren't available, return no clock skew. | ||
guard let httpDateString = response.headers.value(for: "Date") else { return nil } | ||
guard let serverDate = httpDate(httpDateString: httpDateString) else { return nil } | ||
guard let code = (error as? AWSServiceError)?.errorCode else { return nil } | ||
|
||
if definiteClockSkewError(code: code) || probableClockSkewError(code: code, request: request) { | ||
let clockSkew = serverDate.timeIntervalSince(now) | ||
if abs(clockSkew) > clockSkewThreshold { | ||
return clockSkew | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
private static func definiteClockSkewError(code: String) -> Bool { | ||
definiteClockSkewErrorCodes.contains(code) | ||
} | ||
|
||
private static func probableClockSkewError(code: String, request: HTTPRequest) -> Bool { | ||
probableClockSkewErrorCodes.contains(code) // && request.method == .head | ||
} | ||
|
||
private static func httpDate(httpDateString: String) -> Date? { | ||
TimestampFormatter(format: .httpDate).date(from: httpDateString) | ||
} | ||
} | ||
|
||
// These error codes indicate it's likely that the cause of the failure was clock skew. | ||
private let definiteClockSkewErrorCodes = Set([ | ||
"RequestTimeTooSkewed", | ||
"RequestExpired", | ||
"RequestInTheFuture", | ||
]) | ||
|
||
// Some S3 HEAD operations will return these codes instead of the definite codes when the | ||
// operation fails due to clock skew. | ||
private let probableClockSkewErrorCodes = Set([ | ||
"InvalidSignatureException", | ||
"AuthFailure", | ||
"SignatureDoesNotMatch", | ||
]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,7 +57,13 @@ final class RetryIntegrationTests: XCTestCase { | |
.attributes(context) | ||
.retryErrorInfoProvider(DefaultRetryErrorInfoProvider.errorInfo(for:)) | ||
.retryStrategy(subject) | ||
.deserialize({ _, _ in TestOutputResponse() }) | ||
.deserialize({ response, _ in | ||
if response.statusCode == .ok { | ||
return TestOutputResponse() | ||
} else { | ||
throw TestHTTPError(statusCode: response.statusCode) | ||
} | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test file changed comparable to the retry integration tests in smithy-swift. |
||
.executeRequest(next) | ||
builder.interceptors.add(AmzSdkInvocationIdMiddleware()) | ||
builder.interceptors.add(AmzSdkRequestMiddleware(maxRetries: subject.options.maxRetriesBase)) | ||
|
@@ -229,9 +235,10 @@ private class TestOutputHandler: ExecuteRequest { | |
// Return either a successful response or a HTTP error, depending on the directions in the test step. | ||
switch testStep.response { | ||
case .success: | ||
return HTTPResponse() | ||
return HTTPResponse(statusCode: .ok) | ||
case .httpError(let statusCode): | ||
throw TestHTTPError(statusCode: statusCode) | ||
let httpStatusCode = HTTPStatusCode(rawValue: statusCode)! | ||
return HTTPResponse(statusCode: httpStatusCode) | ||
} | ||
} | ||
|
||
|
@@ -310,9 +317,8 @@ private class TestOutputHandler: ExecuteRequest { | |
private struct TestHTTPError: HTTPError, Error { | ||
var httpResponse: HTTPResponse | ||
|
||
init(statusCode: Int) { | ||
guard let statusCodeValue = HTTPStatusCode(rawValue: statusCode) else { fatalError("Unrecognized HTTP code") } | ||
self.httpResponse = HTTPResponse(statusCode: statusCodeValue) | ||
init(statusCode: HTTPStatusCode) { | ||
self.httpResponse = HTTPResponse(statusCode: statusCode) | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import class Smithy.ContextBuilder | |
import class SmithyHTTPAPI.HTTPRequest | ||
import class SmithyHTTPAPI.HTTPResponse | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file & the next few below demonstrate the changes to code-generated clients. |
||
@_spi(SmithyReadWrite) import class SmithyJSON.Writer | ||
import enum AWSClientRuntime.AWSClockSkewProvider | ||
import enum AWSClientRuntime.AWSRetryErrorInfoProvider | ||
import enum AWSClientRuntime.AWSRetryMode | ||
import enum AWSSDKChecksums.AWSChecksumCalculationMode | ||
|
@@ -411,6 +412,7 @@ extension CognitoIdentityClient { | |
builder.interceptors.add(ClientRuntime.ContentLengthMiddleware<GetCredentialsForIdentityInput, GetCredentialsForIdentityOutput>()) | ||
builder.deserialize(ClientRuntime.DeserializeMiddleware<GetCredentialsForIdentityOutput>(GetCredentialsForIdentityOutput.httpOutput(from:), GetCredentialsForIdentityOutputError.httpError(from:))) | ||
builder.interceptors.add(ClientRuntime.LoggerMiddleware<GetCredentialsForIdentityInput, GetCredentialsForIdentityOutput>(clientLogMode: config.clientLogMode)) | ||
builder.clockSkewProvider(AWSClientRuntime.AWSClockSkewProvider.clockSkew(request:response:error:now:)) | ||
builder.retryStrategy(SmithyRetries.DefaultRetryStrategy(options: config.retryStrategyOptions)) | ||
builder.retryErrorInfoProvider(AWSClientRuntime.AWSRetryErrorInfoProvider.errorInfo(for:)) | ||
builder.applySigner(ClientRuntime.SignerMiddleware<GetCredentialsForIdentityOutput>()) | ||
|
@@ -484,6 +486,7 @@ extension CognitoIdentityClient { | |
builder.interceptors.add(ClientRuntime.ContentLengthMiddleware<GetIdInput, GetIdOutput>()) | ||
builder.deserialize(ClientRuntime.DeserializeMiddleware<GetIdOutput>(GetIdOutput.httpOutput(from:), GetIdOutputError.httpError(from:))) | ||
builder.interceptors.add(ClientRuntime.LoggerMiddleware<GetIdInput, GetIdOutput>(clientLogMode: config.clientLogMode)) | ||
builder.clockSkewProvider(AWSClientRuntime.AWSClockSkewProvider.clockSkew(request:response:error:now:)) | ||
builder.retryStrategy(SmithyRetries.DefaultRetryStrategy(options: config.retryStrategyOptions)) | ||
builder.retryErrorInfoProvider(AWSClientRuntime.AWSRetryErrorInfoProvider.errorInfo(for:)) | ||
builder.applySigner(ClientRuntime.SignerMiddleware<GetIdOutput>()) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,9 @@ abstract class AWSHTTPBindingProtocolGenerator( | |
) : HTTPBindingProtocolGenerator(customizations) { | ||
override var serviceErrorProtocolSymbol: Symbol = AWSClientRuntimeTypes.Core.AWSServiceError | ||
|
||
override val clockSkewProviderSymbol: Symbol | ||
get() = AWSClientRuntimeTypes.Core.AWSClockSkewProvider | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overrides the clock skew provider to use the AWS-specific implementation. |
||
override val retryErrorInfoProviderSymbol: Symbol | ||
get() = AWSClientRuntimeTypes.Core.AWSRetryErrorInfoProvider | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,6 +47,7 @@ object AWSClientRuntimeTypes { | |
runtimeSymbol("UnknownAWSHTTPServiceError", SwiftDeclaration.STRUCT, listOf("UnknownAWSHTTPServiceError")) | ||
val AWSServiceError = runtimeSymbol("AWSServiceError", SwiftDeclaration.PROTOCOL) | ||
val Sha256TreeHashMiddleware = runtimeSymbol("Sha256TreeHashMiddleware", SwiftDeclaration.STRUCT) | ||
val AWSClockSkewProvider = runtimeSymbol("AWSClockSkewProvider", SwiftDeclaration.ENUM) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SwiftSymbol for the AWS-specific clock skew provider. |
||
val AWSRetryErrorInfoProvider = runtimeSymbol("AWSRetryErrorInfoProvider", SwiftDeclaration.ENUM) | ||
val AWSRetryMode = runtimeSymbol("AWSRetryMode", SwiftDeclaration.ENUM) | ||
val AWSPartitionDefinition = runtimeSymbol("awsPartitionJSON", SwiftDeclaration.LET) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,13 +33,14 @@ class AWSQueryOperationStackTest { | |
builder.interceptors.add(ClientRuntime.ContentLengthMiddleware<NoInputAndOutputInput, NoInputAndOutputOutput>()) | ||
builder.deserialize(ClientRuntime.DeserializeMiddleware<NoInputAndOutputOutput>(NoInputAndOutputOutput.httpOutput(from:), NoInputAndOutputOutputError.httpError(from:))) | ||
builder.interceptors.add(ClientRuntime.LoggerMiddleware<NoInputAndOutputInput, NoInputAndOutputOutput>(clientLogMode: config.clientLogMode)) | ||
builder.clockSkewProvider(AWSClientRuntime.AWSClockSkewProvider.clockSkew(request:response:error:now:)) | ||
|
||
builder.retryStrategy(SmithyRetries.DefaultRetryStrategy(options: config.retryStrategyOptions)) | ||
builder.retryErrorInfoProvider(AWSClientRuntime.AWSRetryErrorInfoProvider.errorInfo(for:)) | ||
builder.applySigner(ClientRuntime.SignerMiddleware<NoInputAndOutputOutput>()) | ||
let endpointParamsBlock = { [config] (context: Smithy.Context) in | ||
EndpointParams() | ||
} | ||
builder.applyEndpoint(AWSClientRuntime.AWSEndpointResolverMiddleware<NoInputAndOutputOutput, EndpointParams>(paramsBlock: endpointParamsBlock, resolverBlock: { [config] in try config.endpointResolver.resolve(params: ${'$'}0) })) | ||
builder.applyEndpoint(AWSClientRuntime.AWSEndpointResolverMiddleware<NoInputAndOutputOutput, EndpointParams>(paramsBlock: endpointParamsBlock, resolverBlock: { [config] in try config.endpointResolver.resolve(params: $0) })) | ||
builder.serialize(ClientRuntime.BodyMiddleware<NoInputAndOutputInput, NoInputAndOutputOutput, SmithyFormURL.Writer>(rootNodeInfo: "", inputWritingClosure: NoInputAndOutputInput.write(value:to:))) | ||
builder.interceptors.add(ClientRuntime.ContentTypeMiddleware<NoInputAndOutputInput, NoInputAndOutputOutput>(contentType: "application/x-www-form-urlencoded")) | ||
builder.selectAuthScheme(ClientRuntime.AuthSchemeMiddleware<NoInputAndOutputOutput>()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new AWS-specific clock skew provider uses
SmithyTimestamps
to read the date header in HTTP responses, hence the dependency is added.