Skip to content
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

chore(middleware-flexible-checksums): use RequestChecksumCalculation and ResponseChecksumValidation #6492

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
522b524
chore(middleware-flexible-checksums): use RequestChecksumCalculation
trivikr Sep 17, 2024
58dbccb
chore(middleware-flexible-checksums): use ResponseChecksumValidation
trivikr Sep 17, 2024
f3c23ee
chore: deprecate MD5 and use CRC32 by default
trivikr Sep 17, 2024
2159dae
chore(middleware-flexible-checksums): remove ResponseChecksumValidation
trivikr Sep 17, 2024
dce5fb8
chore: change requestChecksumCalculation type to union
trivikr Sep 17, 2024
4532341
test(aws-middleware-test): update MD5 hash to CRC32
trivikr Sep 17, 2024
44f5509
chore: add flexibleChecksumsInterceptorMiddleware to populate input[r…
trivikr Sep 18, 2024
ec690ab
chore: rename interceptor middleware to input
trivikr Sep 19, 2024
4b1b4ff
test: flexibleChecksumsInputMiddleware.spec.ts
trivikr Sep 19, 2024
4d34e54
fix: use SHA256 for default checksum algorithm
trivikr Sep 20, 2024
4d88d5f
fix: revert to use CRC32 for default checksum algorithm
trivikr Oct 4, 2024
4c4d214
chore: use vi in src/flexibleChecksumsInputMiddleware.spec.ts
trivikr Nov 12, 2024
225fdd2
chore: add feature flags for RequestChecksumCalculation and ResponseC…
trivikr Nov 12, 2024
cb480eb
test: use CRC32 as default in flexibleChecksumsMiddleware.spec.ts
trivikr Nov 13, 2024
0367d73
chore: skip checksum computation if user as set checksum header field
trivikr Nov 13, 2024
98b85ec
chore: do not skip checksums for x-amz-checksum-algorithm header
trivikr Nov 13, 2024
c33438b
chore: skip checksums for x-amz-checksum-algorithm header
trivikr Nov 13, 2024
f1ab027
chore: populate requestAlgorithmMemberwhen conditions are met
trivikr Nov 19, 2024
d2d98fb
chore: remove case where input[requestAlgorithmMember] is not set
trivikr Nov 19, 2024
07924c0
test(client-s3): default behavior without change in input
trivikr Nov 20, 2024
a71573d
fix: skip checksum if input[requestAlgorithmMember] is not set
trivikr Nov 21, 2024
9838ff4
test(client-s3): requestChecksumCalculation and responseChecksumValid…
trivikr Nov 21, 2024
f251ada
test(client-s3): fix test types
trivikr Nov 21, 2024
1978c10
test(client-s3): add requestChecksumCalculation for stream
trivikr Nov 21, 2024
d00798e
chore: add utility hasHeaderWithPrefix
trivikr Nov 22, 2024
8835268
chore: leaves input.requestAlgorithmMember if checksum header is set
trivikr Nov 22, 2024
b2757b7
chore: remove check for request headers from flexibleChecksumsInputMi…
trivikr Nov 25, 2024
c54aff5
chore: remove input[requestAlgorithmMember] if set by SDK
trivikr Nov 25, 2024
2381379
chore: use requestAlgorithmMemberHttpHeader to populate header
trivikr Nov 26, 2024
f2d74ac
fix(middleware-flexible-checksums): use object for requestAlgorithmMe…
trivikr Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 198 additions & 137 deletions clients/client-s3/test/unit/flexibleChecksums.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ChecksumAlgorithm } from "@aws-sdk/middleware-flexible-checksums";
import {
ChecksumAlgorithm,
DEFAULT_CHECKSUM_ALGORITHM,
RequestChecksumCalculation,
ResponseChecksumValidation,
} from "@aws-sdk/middleware-flexible-checksums";
import { HttpRequest } from "@smithy/protocol-http";
import { BuildMiddleware } from "@smithy/types";
import { Readable } from "stream";
Expand All @@ -7,7 +12,7 @@ import { describe, expect, test as it } from "vitest";
import { ChecksumAlgorithm as Algo, S3 } from "../../src/index";

describe("Flexible Checksums", () => {
const testCases = [
const testCases: [string, string | undefined, string][] = [
["", ChecksumAlgorithm.CRC32, "AAAAAA=="],
["abc", ChecksumAlgorithm.CRC32, "NSRBwg=="],
["Hello world", ChecksumAlgorithm.CRC32, "i9aeUg=="],
Expand All @@ -23,148 +28,204 @@ describe("Flexible Checksums", () => {
["", ChecksumAlgorithm.SHA256, "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="],
["abc", ChecksumAlgorithm.SHA256, "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="],
["Hello world", ChecksumAlgorithm.SHA256, "ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw="],

// Choose default checksum algorithm when explicily not provided.
["Hello world", undefined, "i9aeUg=="],
];

describe("putObject", () => {
testCases.forEach(([body, checksumAlgorithm, checksumValue]) => {
const checksumHeader = `x-amz-checksum-${checksumAlgorithm.toLowerCase()}`;

describe(`sets ${checksumHeader}="${checksumValue}"" for checksum="${checksumAlgorithm}"`, () => {
const getBodyAsReadableStream = (content: string) => {
const readableStream = new Readable();
const separator = " ";
const wordsAsChunks = content.split(separator);
wordsAsChunks.forEach((word, index) => {
readableStream.push(word);
if (index !== wordsAsChunks.length - 1) {
readableStream.push(separator);
}
});
readableStream.push(null);
return readableStream;
};

it(`when body is sent as a request`, async () => {
const requestChecksumValidator: BuildMiddleware<any, any> = (next) => async (args) => {
// middleware intercept the request and return it early
const request = args.request as HttpRequest;
const { headers } = request;
expect(headers["x-amz-sdk-checksum-algorithm"]).to.equal(checksumAlgorithm);
expect(headers[checksumHeader]).to.equal(checksumValue);
return { output: {} as any, response: {} as any };
};

const client = new S3({
region: "us-west-2",
credentials: {
accessKeyId: "CLIENT_TEST",
secretAccessKey: "CLIENT_TEST",
},
});
client.middlewareStack.addRelativeTo(requestChecksumValidator, {
relation: "after",
toMiddleware: "flexibleChecksumsMiddleware",
});

return await client.putObject({
Bucket: "bucket",
Key: "key",
Body: body,
ChecksumAlgorithm: checksumAlgorithm as Algo,
});
});

it(`when body is sent as a stream`, async () => {
const requestChecksumValidator: BuildMiddleware<any, any> = (next) => async (args) => {
// middleware intercept the request and return it early
const request = args.request as HttpRequest;
const { headers, body } = request;
expect(headers["content-length"]).to.be.undefined;
expect(headers["content-encoding"]).to.equal("aws-chunked");
expect(headers["transfer-encoding"]).to.equal("chunked");
expect(headers["x-amz-content-sha256"]).to.equal("STREAMING-UNSIGNED-PAYLOAD-TRAILER");
expect(headers["x-amz-trailer"]).to.equal(checksumHeader);
body.on("data", (data: any) => {
const stringValue = data.toString();
if (stringValue.startsWith(checksumHeader)) {
const receivedChecksum = stringValue.replace("\r\n", "").split(":")[1];
expect(receivedChecksum).to.equal(checksumValue);
}
describe.each([undefined, RequestChecksumCalculation.WHEN_SUPPORTED, RequestChecksumCalculation.WHEN_REQUIRED])(
`when requestChecksumCalculation='%s'`,
(requestChecksumCalculation) => {
describe.each(testCases)(
`for body="%s" and checksumAlgorithm="%s", sets checksum="%s"`,
(body, checksumAlgorithm, checksumValue) => {
const checksumHeader = `x-amz-checksum-${(checksumAlgorithm ?? DEFAULT_CHECKSUM_ALGORITHM).toLowerCase()}`;
const getBodyAsReadableStream = (content: string) => {
const readableStream = new Readable();
const separator = " ";
const wordsAsChunks = content.split(separator);
wordsAsChunks.forEach((word, index) => {
readableStream.push(word);
if (index !== wordsAsChunks.length - 1) {
readableStream.push(separator);
}
});
readableStream.push(null);
return readableStream;
};

it(`when body is sent as a string`, async () => {
const requestChecksumValidator: BuildMiddleware<any, any> = (next) => async (args) => {
// middleware intercept the request and return it early
const request = args.request as HttpRequest;
const { headers } = request;

// Headers are not set when checksumAlgorithm is not provided,
// and requestChecksumCalculation is explicitly set to WHEN_SUPPORTED.
if (
checksumAlgorithm === undefined &&
requestChecksumCalculation === RequestChecksumCalculation.WHEN_REQUIRED
) {
expect(headers["x-amz-sdk-checksum-algorithm"]).toBeUndefined();
expect(headers[checksumHeader]).toBeUndefined();
} else {
expect(headers["x-amz-sdk-checksum-algorithm"]).toEqual(
checksumAlgorithm ?? DEFAULT_CHECKSUM_ALGORITHM
);
expect(headers[checksumHeader]).toEqual(checksumValue);
}

return { output: {} as any, response: {} as any };
};

const client = new S3({
region: "us-west-2",
credentials: {
accessKeyId: "CLIENT_TEST",
secretAccessKey: "CLIENT_TEST",
},
requestChecksumCalculation,
});
client.middlewareStack.addRelativeTo(requestChecksumValidator, {
relation: "after",
toMiddleware: "flexibleChecksumsMiddleware",
});

return await client.putObject({
Bucket: "bucket",
Key: "key",
Body: body,
ChecksumAlgorithm: checksumAlgorithm as Algo,
});
});

it(`when body is sent as a stream`, async () => {
const requestChecksumValidator: BuildMiddleware<any, any> = (next) => async (args) => {
// middleware intercept the request and return it early
const request = args.request as HttpRequest;
const { headers, body } = request;
expect(headers["content-length"]).toBeUndefined();

// Headers are not set when checksumAlgorithm is not provided,
// and requestChecksumCalculation is explicitly set to WHEN_SUPPORTED.
if (
checksumAlgorithm === undefined &&
requestChecksumCalculation === RequestChecksumCalculation.WHEN_REQUIRED
) {
expect(headers["content-encoding"]).toBeUndefined();
expect(headers["transfer-encoding"]).toBeUndefined();
expect(headers["x-amz-content-sha256"]).toBeUndefined();
expect(headers["x-amz-trailer"]).toBeUndefined();
} else {
expect(headers["content-encoding"]).toEqual("aws-chunked");
expect(headers["transfer-encoding"]).toEqual("chunked");
expect(headers["x-amz-content-sha256"]).toEqual("STREAMING-UNSIGNED-PAYLOAD-TRAILER");
expect(headers["x-amz-trailer"]).toEqual(checksumHeader);
}
body.on("data", (data: any) => {
const stringValue = data.toString();
if (stringValue.startsWith(checksumHeader)) {
const receivedChecksum = stringValue.replace("\r\n", "").split(":")[1];
expect(receivedChecksum).toEqual(checksumValue);
}
});
return { output: {} as any, response: {} as any };
};

const client = new S3({
region: "us-west-2",
credentials: {
accessKeyId: "CLIENT_TEST",
secretAccessKey: "CLIENT_TEST",
},
requestChecksumCalculation,
});
client.middlewareStack.addRelativeTo(requestChecksumValidator, {
relation: "after",
toMiddleware: "flexibleChecksumsMiddleware",
});

const bodyStream = getBodyAsReadableStream(body);
await client.putObject({
Bucket: "bucket",
Key: "key",
Body: bodyStream,
ChecksumAlgorithm: checksumAlgorithm as Algo,
});
});
return { output: {} as any, response: {} as any };
};

const client = new S3({
region: "us-west-2",
credentials: {
accessKeyId: "CLIENT_TEST",
secretAccessKey: "CLIENT_TEST",
},
});
client.middlewareStack.addRelativeTo(requestChecksumValidator, {
relation: "after",
toMiddleware: "flexibleChecksumsMiddleware",
});

const bodyStream = getBodyAsReadableStream(body);
await client.putObject({
Bucket: "bucket",
Key: "key",
Body: bodyStream,
ChecksumAlgorithm: checksumAlgorithm as Algo,
});
});
});
});
}
);
}
);
});

describe("getObject", async () => {
testCases.forEach(([body, checksumAlgorithm, checksumValue]) => {
const checksumHeader = `x-amz-checksum-${checksumAlgorithm.toLowerCase()}`;

it(`validates ${checksumHeader}="${checksumValue}"" set for checksum="${checksumAlgorithm}"`, async () => {
const responseBody = new Readable();
responseBody.push(body);
responseBody.push(null);
const responseChecksumValidator: BuildMiddleware<any, any> = (next, context) => async (args) => {
const request = args.request as HttpRequest;
return {
output: {
$metadata: { attempts: 0, httpStatusCode: 200 },
request,
context,
Body: responseBody,
} as any,
response: {
body: responseBody,
headers: {
[checksumHeader]: checksumValue,
describe.each([undefined, ResponseChecksumValidation.WHEN_SUPPORTED, ResponseChecksumValidation.WHEN_REQUIRED])(
`when responseChecksumValidation='%s'`,
(responseChecksumValidation) => {
it.each(testCases)(
`for body="%s" and checksumAlgorithm="%s", validates ChecksumMode`,
async (body, checksumAlgorithm, checksumValue) => {
const checksumHeader = `x-amz-checksum-${(checksumAlgorithm ?? DEFAULT_CHECKSUM_ALGORITHM).toLowerCase()}`;

const responseBody = new Readable();
responseBody.push(body);
responseBody.push(null);
const responseChecksumValidator: BuildMiddleware<any, any> = (next, context) => async (args) => {
// ChecksumMode is not set when checksumAlgorithm is not provided,
// and responseChecksumValidation is explicitly set to WHEN_SUPPORTED.
if (
checksumAlgorithm === undefined &&
responseChecksumValidation === ResponseChecksumValidation.WHEN_REQUIRED
) {
expect(args.input.ChecksumMode).toBeUndefined();
} else {
expect(args.input.ChecksumMode).toEqual("ENABLED");
}

const request = args.request as HttpRequest;
return {
output: {
$metadata: { attempts: 0, httpStatusCode: 200 },
request,
context,
Body: responseBody,
} as any,
response: {
body: responseBody,
headers: {
[checksumHeader]: checksumValue,
},
} as any,
};
};

const client = new S3({
region: "us-west-2",
credentials: {
accessKeyId: "CLIENT_TEST",
secretAccessKey: "CLIENT_TEST",
},
} as any,
};
};

const client = new S3({
region: "us-west-2",
credentials: {
accessKeyId: "CLIENT_TEST",
secretAccessKey: "CLIENT_TEST",
},
});
client.middlewareStack.addRelativeTo(responseChecksumValidator, {
relation: "after",
toMiddleware: "flexibleChecksumsMiddleware",
});

const { Body } = await client.getObject({
Bucket: "bucket",
Key: "key",
ChecksumMode: "ENABLED",
});
(Body as Readable).on("data", (chunk) => {
expect(chunk.toString()).to.equal(body);
});
});
});
responseChecksumValidation,
});
client.middlewareStack.addRelativeTo(responseChecksumValidator, {
relation: "after",
toMiddleware: "flexibleChecksumsMiddleware",
});

const { Body } = await client.getObject({
Bucket: "bucket",
Key: "key",
// Do not pass ChecksumMode if algorithm is not explicitly defined. It'll be set by SDK.
ChecksumMode: checksumAlgorithm ? "ENABLED" : undefined,
});
(Body as Readable).on("data", (chunk) => {
expect(chunk.toString()).toEqual(body);
});
}
);
}
);
});
});
13 changes: 13 additions & 0 deletions packages/middleware-flexible-checksums/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
Encoder,
GetAwsChunkedEncodingStream,
HashConstructor,
Provider,
StreamCollector,
StreamHasher,
} from "@smithy/types";

import { RequestChecksumCalculation, ResponseChecksumValidation } from "./constants";

export interface PreviouslyResolved {
/**
* The function that will be used to convert binary data to a base64-encoded string.
Expand All @@ -31,6 +34,16 @@ export interface PreviouslyResolved {
*/
md5: ChecksumConstructor | HashConstructor;

/**
* Determines when a checksum will be calculated for request payloads
*/
requestChecksumCalculation: Provider<RequestChecksumCalculation>;

/**
* Determines when a checksum will be calculated for response payloads
*/
responseChecksumValidation: Provider<ResponseChecksumValidation>;

/**
* A constructor for a class implementing the {@link Hash} interface that computes SHA1 hashes.
* @internal
Expand Down
Loading
Loading