Skip to content

Commit

Permalink
chore(sigv4): Refactor canonical request and service configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
Dillon Nys committed Mar 29, 2022
1 parent 4da784d commit ea381d2
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,29 @@ abstract class ServiceConfiguration {
/// Whether to omit the session token during signing.
final bool omitSessionToken;

/// Applies service-specific keys to [base] with values from [canonicalRequest]
/// and [credentials].
/// Applies service-specific keys to [headers] for signed header requests.
@mustCallSuper
void apply(
Map<String, String> base,
CanonicalRequest canonicalRequest, {
void applySigned(
Map<String, String> headers, {
required AWSBaseHttpRequest request,
required AWSCredentialScope credentialScope,
required AWSCredentials credentials,
required String payloadHash,
required int contentLength,
});

/// Whether to include the body hash in the signing process.
bool includeBodyHash(
CanonicalRequest request,
int contentLength,
);
/// Applies service-specific keys to [queryParameters] for pre-signed URL
/// requests.
@mustCallSuper
void applyPresigned(
Map<String, String> queryParameters, {
required AWSBaseHttpRequest request,
required AWSCredentialScope credentialScope,
required AWSAlgorithm algorithm,
required SignedHeaders signedHeaders,
required int expiresIn,
required AWSCredentials credentials,
});

/// Hashes the request payload for the canonical request.
Future<String> hashPayload(
Expand Down Expand Up @@ -101,45 +108,46 @@ class BaseServiceConfiguration extends ServiceConfiguration {
);

@override
void apply(
Map<String, String> base,
CanonicalRequest canonicalRequest, {
void applySigned(
Map<String, String> headers, {
required AWSBaseHttpRequest request,
required AWSCredentialScope credentialScope,
required AWSCredentials credentials,
required String payloadHash,
required int contentLength,
}) {
final request = canonicalRequest.request;
final presignedUrl = canonicalRequest.presignedUrl;
final credentialScope = canonicalRequest.credentialScope;
final algorithm = canonicalRequest.algorithm;
final expiresIn = canonicalRequest.expiresIn;
final omitSessionTokenFromSigning =
canonicalRequest.omitSessionTokenFromSigning;

base.addAll({
final includeBodyHash = contentLength > 0;
headers.addAll({
if (!request.headers.containsKey(AWSHeaders.host))
AWSHeaders.host: request.host,
AWSHeaders.date: credentialScope.dateTime.formatFull(),
if (presignedUrl)
AWSHeaders.signedHeaders: canonicalRequest.signedHeaders.toString(),
if (presignedUrl && algorithm != null) AWSHeaders.algorithm: algorithm.id,
if (presignedUrl)
AWSHeaders.credential: '${credentials.accessKeyId}/$credentialScope',
if (presignedUrl && expiresIn != null)
AWSHeaders.expires: expiresIn.toString(),
if (includeBodyHash(canonicalRequest, contentLength))
AWSHeaders.contentSHA256: canonicalRequest.payloadHash,
if (credentials.sessionToken != null && !omitSessionTokenFromSigning)
if (includeBodyHash) AWSHeaders.contentSHA256: payloadHash,
if (credentials.sessionToken != null && !omitSessionToken)
AWSHeaders.securityToken: credentials.sessionToken!,
});
}

@override
bool includeBodyHash(
CanonicalRequest request,
int contentLength,
) {
return !request.presignedUrl && contentLength > 0;
void applyPresigned(
Map<String, String> queryParameters, {
required AWSBaseHttpRequest request,
required AWSCredentialScope credentialScope,
required AWSAlgorithm algorithm,
required SignedHeaders signedHeaders,
required int expiresIn,
required AWSCredentials credentials,
}) {
queryParameters.addAll({
if (!request.headers.containsKey(AWSHeaders.host))
AWSHeaders.host: request.host,
AWSHeaders.date: credentialScope.dateTime.formatFull(),
AWSHeaders.signedHeaders: signedHeaders.toString(),
AWSHeaders.algorithm: algorithm.id,
AWSHeaders.credential: '${credentials.accessKeyId}/$credentialScope',
AWSHeaders.expires: expiresIn.toString(),
if (credentials.sessionToken != null && !omitSessionToken)
AWSHeaders.securityToken: credentials.sessionToken!,
});
}

@override
Expand Down
32 changes: 15 additions & 17 deletions packages/aws_signature_v4/lib/src/configuration/services/s3.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,44 +96,42 @@ class S3ServiceConfiguration extends BaseServiceConfiguration {
}

@override
void apply(
Map<String, String> base,
CanonicalRequest canonicalRequest, {
void applySigned(
Map<String, String> headers, {
required AWSBaseHttpRequest request,
required AWSCredentialScope credentialScope,
required AWSCredentials credentials,
required String payloadHash,
required int contentLength,
}) {
super.apply(
base,
canonicalRequest,
super.applySigned(
headers,
request: request,
credentialScope: credentialScope,
credentials: credentials,
payloadHash: payloadHash,
contentLength: contentLength,
);

if (canonicalRequest.presignedUrl) {
return;
}

if (chunked) {
// Raw size of the data to be sent, before compression and without metadata.
base[AWSHeaders.decodedContentLength] = contentLength.toString();
headers[AWSHeaders.decodedContentLength] = contentLength.toString();

if (encoding == S3PayloadEncoding.none) {
base[AWSHeaders.contentEncoding] = 'aws-chunked';
base[AWSHeaders.contentLength] = _calculateContentLength(
canonicalRequest.request,
headers[AWSHeaders.contentEncoding] = 'aws-chunked';
headers[AWSHeaders.contentLength] = _calculateContentLength(
request,
contentLength,
).toString();
} else {
base[AWSHeaders.contentEncoding] = 'aws-chunked,${encoding.value}';
headers[AWSHeaders.contentEncoding] = 'aws-chunked,${encoding.value}';
}
}

if (signPayload) {
base[AWSHeaders.contentSHA256] = payloadHash;
headers[AWSHeaders.contentSHA256] = payloadHash;
} else {
base[AWSHeaders.contentSHA256] = 'UNSIGNED-PAYLOAD';
headers[AWSHeaders.contentSHA256] = 'UNSIGNED-PAYLOAD';
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,19 @@ class CanonicalRequest {
);

/// The request query parameters, with AWS values added, if necessary.
late final Map<String, String> queryParameters;
final Map<String, String> queryParameters;

/// The canonicalized [queryParameters].
late final CanonicalQueryParameters canonicalQueryParameters;
final CanonicalQueryParameters canonicalQueryParameters;

/// The request headers, with AWS values added, if necessary.
late final Map<String, String> headers;
final Map<String, String> headers;

/// The canonicalized [headers].
late final CanonicalHeaders canonicalHeaders;
final CanonicalHeaders canonicalHeaders;

/// The list of signed headers.
late final SignedHeaders signedHeaders;
final SignedHeaders signedHeaders;

/// Whether or not to normalize the URI path.
///
Expand Down Expand Up @@ -91,10 +91,10 @@ class CanonicalRequest {
///
/// Only valid for presigned URLs, and must be provided if [presignedUrl]
/// is `true`.
late final int? expiresIn;
final int? expiresIn;

/// The configuration to use for canonicalizing the request.
final ServiceConfiguration configuration;
final ServiceConfiguration serviceConfiguration;

/// The payload content length.
final int contentLength;
Expand All @@ -106,54 +106,114 @@ class CanonicalRequest {
final String payloadHash;

/// {@macro aws_signature_v4.canonical_request}
CanonicalRequest({
required this.request,
factory CanonicalRequest({
required AWSBaseHttpRequest request,
required AWSCredentials credentials,
required AWSCredentialScope credentialScope,
required int contentLength,
required String payloadHash,
ServiceConfiguration serviceConfiguration =
const BaseServiceConfiguration(),
}) {
final headers = Map.of(request.headers);
final queryParameters = Map.of(request.queryParameters);

serviceConfiguration.applySigned(
headers,
request: request,
credentialScope: credentialScope,
credentials: credentials,
payloadHash: payloadHash,
contentLength: contentLength,
);
final canonicalQueryParameters = CanonicalQueryParameters(queryParameters);
final canonicalHeaders = CanonicalHeaders(headers);
final signedHeaders = SignedHeaders(canonicalHeaders);

return CanonicalRequest._(
request: request,
queryParameters: queryParameters,
canonicalQueryParameters: canonicalQueryParameters,
headers: headers,
canonicalHeaders: canonicalHeaders,
signedHeaders: signedHeaders,
credentialScope: credentialScope,
presignedUrl: false,
contentLength: contentLength,
payloadHash: payloadHash,
serviceConfiguration: serviceConfiguration,
);
}

/// {@macro aws_signature_v4.canonical_request}
factory CanonicalRequest.presignedUrl({
required AWSBaseHttpRequest request,
required AWSCredentials credentials,
required AWSCredentialScope credentialScope,
required AWSAlgorithm algorithm,
required Duration expiresIn,
int contentLength = 0,
String payloadHash = emptyPayloadHash,
ServiceConfiguration serviceConfiguration =
const BaseServiceConfiguration(),
}) {
final headers = Map.of(request.headers);
final queryParameters = Map.of(request.queryParameters);

// Per https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
assert(
expiresIn > const Duration(seconds: 1) &&
expiresIn < const Duration(days: 7),
'Expiration must be greater than 1 second and less than 7 days',
);

final canonicalHeaders = CanonicalHeaders(headers);
final signedHeaders = SignedHeaders(canonicalHeaders);
serviceConfiguration.applyPresigned(
queryParameters,
request: request,
credentialScope: credentialScope,
algorithm: algorithm,
expiresIn: expiresIn.inSeconds,
signedHeaders: signedHeaders,
credentials: credentials,
);

return CanonicalRequest._(
request: request,
queryParameters: queryParameters,
canonicalQueryParameters: CanonicalQueryParameters(queryParameters),
headers: headers,
canonicalHeaders: canonicalHeaders,
signedHeaders: signedHeaders,
credentialScope: credentialScope,
algorithm: algorithm,
expiresIn: expiresIn,
presignedUrl: true,
contentLength: contentLength,
payloadHash: payloadHash,
serviceConfiguration: serviceConfiguration,
);
}

/// {@macro aws_signature_v4.canonical_request}
CanonicalRequest._({
required this.request,
required this.queryParameters,
required this.canonicalQueryParameters,
required this.headers,
required this.canonicalHeaders,
required this.signedHeaders,
required this.credentialScope,
required this.algorithm,
this.contentLength = 0,
this.payloadHash = emptyPayloadHash,
this.configuration = const BaseServiceConfiguration(),
this.presignedUrl = false,
required this.contentLength,
required this.payloadHash,
required this.serviceConfiguration,
required this.presignedUrl,
this.algorithm,
Duration? expiresIn,
}) : normalizePath = configuration.normalizePath,
omitSessionTokenFromSigning = configuration.omitSessionToken {
headers = Map.of(request.headers);
queryParameters = Map.of(request.queryParameters);

// Apply service configuration to appropriate values for request type.
if (presignedUrl) {
ArgumentError.checkNotNull(expiresIn, 'expiresIn');
this.expiresIn = expiresIn!.inSeconds;
// Per https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
assert(
expiresIn > const Duration(seconds: 1) &&
expiresIn < const Duration(days: 7),
'Expiration must be greater than 1 second and less than 7 days',
);
canonicalHeaders = CanonicalHeaders(headers);
signedHeaders = SignedHeaders(canonicalHeaders);
configuration.apply(
queryParameters,
this,
credentials: credentials,
payloadHash: payloadHash,
contentLength: contentLength,
);
} else {
this.expiresIn = null;
configuration.apply(
headers,
this,
credentials: credentials,
payloadHash: payloadHash,
contentLength: contentLength,
);
canonicalHeaders = CanonicalHeaders(headers);
signedHeaders = SignedHeaders(canonicalHeaders);
}
canonicalQueryParameters = CanonicalQueryParameters(queryParameters);
}
}) : normalizePath = serviceConfiguration.normalizePath,
omitSessionTokenFromSigning = serviceConfiguration.omitSessionToken,
expiresIn = expiresIn?.inSeconds;

/// Returns the normalized path with double-encoded path segments.
///
Expand Down
Loading

0 comments on commit ea381d2

Please sign in to comment.