Skip to content

Commit 66e1587

Browse files
committed
feat(middleware-flexible-checksums): allow custom checksums to be used in responses
1 parent 7888030 commit 66e1587

File tree

4 files changed

+149
-18
lines changed

4 files changed

+149
-18
lines changed

packages-internal/middleware-flexible-checksums/src/flexibleChecksumsResponseMiddleware.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,32 @@ export const flexibleChecksumsResponseMiddleware =
7373
// @ts-ignore Element implicitly has an 'any' type for input[requestValidationModeMember]
7474
if (requestValidationModeMember && input[requestValidationModeMember] === "ENABLED") {
7575
const { clientName, commandName } = context;
76+
77+
const customChecksumAlgorithms = Object.keys(config.checksumAlgorithms ?? {}).filter((algorithm: string) => {
78+
const responseHeader = getChecksumLocationName(algorithm);
79+
return response.headers[responseHeader] !== undefined;
80+
});
81+
const algoList = getChecksumAlgorithmListForResponse([
82+
...(responseAlgorithms ?? []),
83+
...customChecksumAlgorithms,
84+
]);
85+
7686
const isS3WholeObjectMultipartGetResponseChecksum =
7787
clientName === "S3Client" &&
7888
commandName === "GetObjectCommand" &&
79-
getChecksumAlgorithmListForResponse(responseAlgorithms).every((algorithm: ChecksumAlgorithm) => {
89+
algoList.every((algorithm: ChecksumAlgorithm) => {
8090
const responseHeader = getChecksumLocationName(algorithm);
8191
const checksumFromResponse = response.headers[responseHeader];
8292
return !checksumFromResponse || isChecksumWithPartNumber(checksumFromResponse);
8393
});
94+
8495
if (isS3WholeObjectMultipartGetResponseChecksum) {
8596
return result;
8697
}
8798

8899
await validateChecksumFromResponse(response as HttpResponse, {
89100
config,
90-
responseAlgorithms,
101+
responseAlgorithms: algoList,
91102
logger: context.logger,
92103
});
93104
}

packages-internal/middleware-flexible-checksums/src/getChecksumAlgorithmListForResponse.spec.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { getChecksumAlgorithmListForResponse } from "./getChecksumAlgorithmListF
44
import { PRIORITY_ORDER_ALGORITHMS } from "./types";
55

66
describe(getChecksumAlgorithmListForResponse.name, () => {
7-
const unknownAlgorithm = "UNKNOWNALGO";
7+
const u1 = "UNKNOWNALGO1";
8+
const u2 = "UNKNOWNALGO2";
9+
const u3 = "UNKNOWNALGO3";
810

911
it("returns empty if responseAlgorithms is empty", () => {
1012
expect(getChecksumAlgorithmListForResponse([])).toEqual([]);
1113
});
1214

13-
it("returns empty if contents of responseAlgorithms is not in priority order", () => {
14-
expect(getChecksumAlgorithmListForResponse([unknownAlgorithm])).toEqual([]);
15+
it("returns unknown algorithms in their existing order if no priority information is available", () => {
16+
expect(getChecksumAlgorithmListForResponse([u1, u3, u2])).toEqual([u1, u3, u2]);
1517
});
1618

1719
describe("returns list as per priority order", () => {
@@ -38,12 +40,14 @@ describe(getChecksumAlgorithmListForResponse.name, () => {
3840
);
3941
});
4042

41-
it("ignores algorithms not present in priority list", () => {
42-
expect(getChecksumAlgorithmListForResponse([unknownAlgorithm, ...PRIORITY_ORDER_ALGORITHMS].reverse())).toEqual(
43-
PRIORITY_ORDER_ALGORITHMS
44-
);
45-
expect(getChecksumAlgorithmListForResponse([...PRIORITY_ORDER_ALGORITHMS, unknownAlgorithm].reverse())).toEqual(
46-
PRIORITY_ORDER_ALGORITHMS
47-
);
43+
it("does not ignore algorithms not present in the priority list. However, they receive lowest priority.", () => {
44+
expect(getChecksumAlgorithmListForResponse([u1, ...PRIORITY_ORDER_ALGORITHMS].reverse())).toEqual([
45+
...PRIORITY_ORDER_ALGORITHMS,
46+
u1,
47+
]);
48+
expect(getChecksumAlgorithmListForResponse([...PRIORITY_ORDER_ALGORITHMS, u1].reverse())).toEqual([
49+
...PRIORITY_ORDER_ALGORITHMS,
50+
u1,
51+
]);
4852
});
4953
});
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ChecksumAlgorithm } from "./constants";
2-
import { CLIENT_SUPPORTED_ALGORITHMS, PRIORITY_ORDER_ALGORITHMS } from "./types";
2+
import { PRIORITY_ORDER_ALGORITHMS } from "./types";
33

44
/**
55
* Returns the priority array of algorithm to use to verify checksum and names
@@ -8,12 +8,16 @@ import { CLIENT_SUPPORTED_ALGORITHMS, PRIORITY_ORDER_ALGORITHMS } from "./types"
88
export const getChecksumAlgorithmListForResponse = (responseAlgorithms: string[] = []): ChecksumAlgorithm[] => {
99
const validChecksumAlgorithms: ChecksumAlgorithm[] = [];
1010

11-
for (const algorithm of PRIORITY_ORDER_ALGORITHMS) {
12-
if (!responseAlgorithms.includes(algorithm) || !CLIENT_SUPPORTED_ALGORITHMS.includes(algorithm)) {
13-
continue;
11+
let i = PRIORITY_ORDER_ALGORITHMS.length;
12+
13+
for (const algorithm of responseAlgorithms) {
14+
const priority = PRIORITY_ORDER_ALGORITHMS.indexOf(algorithm as ChecksumAlgorithm);
15+
if (priority !== -1) {
16+
validChecksumAlgorithms[priority] = algorithm as ChecksumAlgorithm;
17+
} else {
18+
validChecksumAlgorithms[i++] = algorithm as ChecksumAlgorithm;
1419
}
15-
validChecksumAlgorithms.push(algorithm as ChecksumAlgorithm);
1620
}
1721

18-
return validChecksumAlgorithms;
22+
return validChecksumAlgorithms.filter(Boolean);
1923
};

packages-internal/middleware-flexible-checksums/src/middleware-flexible-checksums.integ.spec.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { requireRequestsFrom } from "@aws-sdk/aws-util-test/src";
2+
import type { S3ExtensionConfiguration } from "@aws-sdk/client-s3";
23
import { ChecksumAlgorithm, S3 } from "@aws-sdk/client-s3";
34
import type { HttpHandler, HttpRequest } from "@smithy/protocol-http";
45
import { HttpResponse } from "@smithy/protocol-http";
6+
import type { Checksum } from "@smithy/types";
7+
import { toBase64 } from "@smithy/util-base64";
8+
import { ChecksumStream } from "@smithy/util-stream";
9+
import { fromUtf8 } from "@smithy/util-utf8";
510
import { Readable, Transform } from "node:stream";
611
import { describe, expect, test as it } from "vitest";
712

@@ -236,4 +241,111 @@ describe("middleware-flexible-checksums", () => {
236241
});
237242
});
238243
});
244+
245+
describe("Novel checksums", () => {
246+
/**
247+
* This highly performant checksum algorithm always returns the bytes of the string "Hello, world."
248+
* and the number of bytes seen.
249+
*/
250+
class HelloWorldChecksum implements Checksum {
251+
private hash = "Hello, world.";
252+
private bytesSeen = 0;
253+
254+
public constructor() {}
255+
256+
public reset(): void {
257+
this.bytesSeen = 0;
258+
}
259+
260+
public update(data: Uint8Array): void {
261+
this.bytesSeen += data.byteLength;
262+
}
263+
264+
async digest(): Promise<Uint8Array> {
265+
return fromUtf8(this.hash + this.bytesSeen);
266+
}
267+
}
268+
269+
class HelloWorldChecksumExtension {
270+
configure(extensions: S3ExtensionConfiguration) {
271+
extensions.addChecksumAlgorithm({
272+
algorithmId() {
273+
return "HELLOWORLD";
274+
},
275+
checksumConstructor() {
276+
return HelloWorldChecksum;
277+
},
278+
});
279+
}
280+
}
281+
282+
describe("novel request checksum", () => {
283+
it("should send a request with the novel checksum in the header", async () => {
284+
const s3 = new S3({
285+
credentials: {
286+
accessKeyId: "INTEG",
287+
secretAccessKey: "INTEG",
288+
},
289+
extensions: [new HelloWorldChecksumExtension()],
290+
});
291+
requireRequestsFrom(s3).toMatch({
292+
headers: {
293+
"x-amz-checksum-helloworld": toBase64(fromUtf8("Hello, world.2")),
294+
},
295+
});
296+
297+
await s3.putObject({
298+
Bucket: "bucket",
299+
Key: "key",
300+
Body: "hi",
301+
ChecksumAlgorithm: "HELLOWORLD" as any,
302+
});
303+
304+
expect.assertions(1);
305+
});
306+
307+
it("should receive a request and verify the novel checksum", async () => {
308+
const s3 = new S3({
309+
credentials: {
310+
accessKeyId: "INTEG",
311+
secretAccessKey: "INTEG",
312+
},
313+
extensions: [new HelloWorldChecksumExtension()],
314+
});
315+
316+
requireRequestsFrom(s3)
317+
.toMatch({
318+
hostname: "bucket.s3.us-west-2.amazonaws.com",
319+
})
320+
.respondWith(
321+
new HttpResponse({
322+
statusCode: 200,
323+
headers: {
324+
"x-amz-checksum-helloworld": toBase64(fromUtf8("Hello, world.2")),
325+
},
326+
body: Readable.from(Buffer.from("hi_extra_bytes")),
327+
})
328+
);
329+
330+
const get = await s3.getObject({
331+
Bucket: "bucket",
332+
Key: "key",
333+
});
334+
335+
expect.assertions(3);
336+
337+
expect(get.Body).toBeInstanceOf(ChecksumStream);
338+
try {
339+
await get.Body?.transformToByteArray();
340+
} catch (e) {
341+
const [ex, ac] = [toBase64(fromUtf8("Hello, world.2")), toBase64(fromUtf8("Hello, world.14"))];
342+
expect(e.message).toEqual(
343+
`
344+
Checksum mismatch: expected "${ex}" but received "${ac}" in response header "x-amz-checksum-helloworld".
345+
`.trim()
346+
);
347+
}
348+
});
349+
});
350+
});
239351
});

0 commit comments

Comments
 (0)