Skip to content

Commit 213defa

Browse files
authored
feat(middleware-flexible-checksums): allow custom checksums to be used in responses (#7849)
1 parent 7888030 commit 213defa

File tree

4 files changed

+219
-18
lines changed

4 files changed

+219
-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: 18 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,18 @@ 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, u3, ...PRIORITY_ORDER_ALGORITHMS, u2].reverse())).toEqual([
45+
...PRIORITY_ORDER_ALGORITHMS,
46+
u2,
47+
u3,
48+
u1,
49+
]);
50+
expect(getChecksumAlgorithmListForResponse([u2, ...PRIORITY_ORDER_ALGORITHMS, u3, u1].reverse())).toEqual([
51+
...PRIORITY_ORDER_ALGORITHMS,
52+
u1,
53+
u3,
54+
u2,
55+
]);
4856
});
4957
});
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: 178 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,177 @@ 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 if the implementation is provided", 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 throw an error if the requested algorithm implementation is not available", async () => {
308+
const s3 = new S3({
309+
credentials: {
310+
accessKeyId: "INTEG",
311+
secretAccessKey: "INTEG",
312+
},
313+
extensions: [],
314+
});
315+
requireRequestsFrom(s3).toMatch({
316+
headers: {
317+
"x-amz-checksum-helloworld": toBase64(fromUtf8("Hello, world.2")),
318+
},
319+
});
320+
321+
try {
322+
await s3.putObject({
323+
Bucket: "bucket",
324+
Key: "key",
325+
Body: "hi",
326+
ChecksumAlgorithm: "HELLOWORLD" as any,
327+
});
328+
} catch (e) {
329+
expect(e.message).toMatch(/The checksum algorithm "HELLOWORLD" is not supported by the client/);
330+
}
331+
332+
expect.assertions(1);
333+
});
334+
});
335+
336+
describe("novel response checksum", () => {
337+
it("should receive a request and verify the novel checksum", async () => {
338+
const s3 = new S3({
339+
credentials: {
340+
accessKeyId: "INTEG",
341+
secretAccessKey: "INTEG",
342+
},
343+
extensions: [new HelloWorldChecksumExtension()],
344+
});
345+
346+
requireRequestsFrom(s3)
347+
.toMatch({
348+
hostname: "bucket.s3.us-west-2.amazonaws.com",
349+
})
350+
.respondWith(
351+
new HttpResponse({
352+
statusCode: 200,
353+
headers: {
354+
"x-amz-checksum-helloworld": toBase64(fromUtf8("Hello, world.2")),
355+
},
356+
body: Readable.from(Buffer.from("hi_extra_bytes")),
357+
})
358+
);
359+
360+
const get = await s3.getObject({
361+
Bucket: "bucket",
362+
Key: "key",
363+
});
364+
365+
expect.assertions(3);
366+
367+
expect(get.Body).toBeInstanceOf(ChecksumStream);
368+
try {
369+
await get.Body?.transformToByteArray();
370+
} catch (e) {
371+
const [ex, ac] = [toBase64(fromUtf8("Hello, world.2")), toBase64(fromUtf8("Hello, world.14"))];
372+
expect(e.message).toEqual(
373+
`
374+
Checksum mismatch: expected "${ex}" but received "${ac}" in response header "x-amz-checksum-helloworld".
375+
`.trim()
376+
);
377+
}
378+
});
379+
380+
it("should ignore the checksum header and perform no checksum validation if no matching algorithm implementation is available", async () => {
381+
const s3 = new S3({
382+
credentials: {
383+
accessKeyId: "INTEG",
384+
secretAccessKey: "INTEG",
385+
},
386+
extensions: [],
387+
});
388+
389+
requireRequestsFrom(s3)
390+
.toMatch({
391+
hostname: "bucket.s3.us-west-2.amazonaws.com",
392+
})
393+
.respondWith(
394+
new HttpResponse({
395+
statusCode: 200,
396+
headers: {
397+
"x-amz-checksum-helloworld": toBase64(fromUtf8("Hello, world.2")),
398+
},
399+
body: Readable.from(Buffer.from("hi_extra_bytes")),
400+
})
401+
);
402+
403+
const get = await s3.getObject({
404+
Bucket: "bucket",
405+
Key: "key",
406+
});
407+
408+
expect.assertions(3);
409+
410+
expect(get.Body).not.toBeInstanceOf(ChecksumStream);
411+
412+
const objectContent = await get.Body?.transformToString();
413+
expect(objectContent).toEqual("hi_extra_bytes");
414+
});
415+
});
416+
});
239417
});

0 commit comments

Comments
 (0)