From 47affd26d928a7e49c97f46b1095ff86e08057d7 Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 20 Sep 2024 20:40:22 +0000 Subject: [PATCH] chore(middleware-user-agent): update to user agent 2.1 spec --- .../src/encode-metrics.spec.ts | 26 +++++++ .../src/encode-metrics.ts | 28 ++++++++ .../src/middleware-user-agent.integ.spec.ts | 2 +- .../src/user-agent-middleware.spec.ts | 31 ++++++++ .../src/user-agent-middleware.ts | 10 ++- packages/types/src/middleware.ts | 72 +++++++++++++++++++ .../src/index.native.spec.ts | 2 +- .../src/index.native.ts | 2 +- .../util-user-agent-browser/src/index.spec.ts | 2 +- packages/util-user-agent-browser/src/index.ts | 2 +- .../util-user-agent-node/src/index.spec.ts | 2 +- packages/util-user-agent-node/src/index.ts | 2 +- 12 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 packages/middleware-user-agent/src/encode-metrics.spec.ts create mode 100644 packages/middleware-user-agent/src/encode-metrics.ts diff --git a/packages/middleware-user-agent/src/encode-metrics.spec.ts b/packages/middleware-user-agent/src/encode-metrics.spec.ts new file mode 100644 index 000000000000..0b1ceb7e9de2 --- /dev/null +++ b/packages/middleware-user-agent/src/encode-metrics.spec.ts @@ -0,0 +1,26 @@ +import { encodeMetrics } from "./encode-metrics"; + +describe(encodeMetrics.name, () => { + it("encodes empty metrics", () => { + expect(encodeMetrics({})).toEqual(""); + }); + + it("encodes metrics", () => { + expect( + encodeMetrics({ + A: "A", + z: "z", + } as any) + ).toEqual("A,z"); + }); + + it("drops values that would exceed 1024 bytes", () => { + expect( + encodeMetrics({ + A: "A".repeat(512), + B: "B".repeat(511), + z: "z", + } as any) + ).toEqual("A".repeat(512) + "," + "B".repeat(511)); + }); +}); diff --git a/packages/middleware-user-agent/src/encode-metrics.ts b/packages/middleware-user-agent/src/encode-metrics.ts new file mode 100644 index 000000000000..2136a3f7372d --- /dev/null +++ b/packages/middleware-user-agent/src/encode-metrics.ts @@ -0,0 +1,28 @@ +import type { AwsSdkFeatures } from "@aws-sdk/types"; + +const BYTE_LIMIT = 1024; + +/** + * @internal + */ +export function encodeMetrics(metrics: AwsSdkFeatures): string { + let buffer = ""; + + // currently all possible values are 1 byte, + // so string length is used. + + for (const key in metrics) { + const val = metrics[key as keyof typeof metrics]!; + if (buffer.length + val!.length + 1 <= BYTE_LIMIT) { + if (buffer.length) { + buffer += "," + val; + } else { + buffer += val; + } + continue; + } + break; + } + + return buffer; +} diff --git a/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts b/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts index f95b8f78cf19..3ba05bd90f58 100644 --- a/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts +++ b/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts @@ -14,7 +14,7 @@ describe("middleware-user-agent", () => { requireRequestsFrom(client).toMatch({ headers: { "x-amz-user-agent": /aws-sdk-js\/[\d\.]+/, - "user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+/, + "user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+ m\//, }, }); await client.getUserDetails({ diff --git a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts index b971fb2588f1..dc3bd44d2e52 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts @@ -50,6 +50,37 @@ describe("userAgentMiddleware", () => { ); }); + describe("metrics", () => { + it("should collect metrics from the context", async () => { + const middleware = userAgentMiddleware({ + defaultUserAgentProvider: async () => [ + ["default_agent", "1.0.0"], + ["aws-sdk-js", "1.0.0"], + ], + runtime: "node", + }); + + const handler = middleware(mockNextHandler, { + __aws_sdk_context: { + features: { + "0": "0", + "9": "9", + A: "A", + B: "B", + y: "y", + z: "z", + "+": "+", + "/": "/", + }, + }, + }); + await handler({ input: {}, request: new HttpRequest({ headers: {} }) }); + expect(mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT]).toEqual( + expect.stringContaining(`m/0,9,A,B,y,z,+,/`) + ); + }); + }); + describe("should sanitize the SDK user agent string", () => { const cases: { ua: UserAgentPair; expected: string }[] = [ { ua: ["/name", "1.0.0"], expected: "name/1.0.0" }, diff --git a/packages/middleware-user-agent/src/user-agent-middleware.ts b/packages/middleware-user-agent/src/user-agent-middleware.ts index 456f967b7d8a..f95e191aa299 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.ts @@ -1,3 +1,4 @@ +import type { AwsHandlerExecutionContext } from "@aws-sdk/types"; import { getUserAgentPrefix } from "@aws-sdk/util-endpoints"; import { HttpRequest } from "@smithy/protocol-http"; import { @@ -22,6 +23,7 @@ import { USER_AGENT, X_AMZ_USER_AGENT, } from "./constants"; +import { encodeMetrics } from "./encode-metrics"; /** * Build user agent header sections from: @@ -39,14 +41,18 @@ export const userAgentMiddleware = (options: UserAgentResolvedConfig) => ( next: BuildHandler, - context: HandlerExecutionContext + context: HandlerExecutionContext | AwsHandlerExecutionContext ): BuildHandler => async (args: BuildHandlerArguments): Promise> => { const { request } = args; - if (!HttpRequest.isInstance(request)) return next(args); + if (!HttpRequest.isInstance(request)) { + return next(args); + } const { headers } = request; const userAgent = context?.userAgent?.map(escapeUserAgent) || []; const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent); + const awsContext = context as AwsHandlerExecutionContext; + defaultUserAgent.push(`m/${encodeMetrics(awsContext.__aws_sdk_context?.features ?? {})}`); const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || []; const prefix = getUserAgentPrefix(); diff --git a/packages/types/src/middleware.ts b/packages/types/src/middleware.ts index 3ae51bd9a83d..da871aeed803 100644 --- a/packages/types/src/middleware.ts +++ b/packages/types/src/middleware.ts @@ -1,3 +1,5 @@ +import { HandlerExecutionContext } from "@smithy/types"; + export { AbsoluteLocation, BuildHandler, @@ -38,3 +40,73 @@ export { Step, Terminalware, } from "@smithy/types"; + +/** + * @internal + * Contains reserved keys for AWS SDK internal usage of the + * handler execution context object. + */ +export interface AwsHandlerExecutionContext extends HandlerExecutionContext { + __aws_sdk_context?: { + features?: AwsSdkFeatures; + }; +} + +/** + * @internal + */ +export type AwsSdkFeatures = Partial<{ + RESOURCE_MODEL: "A"; + WAITER: "B"; + PAGINATOR: "C"; + RETRY_MODE_LEGACY: "D"; + RETRY_MODE_STANDARD: "E"; + RETRY_MODE_ADAPTIVE: "F"; + // S3_TRANSFER: "G"; // not applicable. + // S3_CRYPTO_V1N: "H"; // not applicable. + // S3_CRYPTO_V2: "I"; // not applicable. + S3_EXPRESS_BUCKET: "J"; + S3_ACCESS_GRANTS: "K"; + GZIP_REQUEST_COMPRESSION: "L"; + PROTOCOL_RPC_V2_CBOR: "M"; + ENDPOINT_OVERRIDE: "N"; + ACCOUNT_ID_ENDPOINT: "O"; + ACCOUNT_ID_MODE_PREFERRED: "P"; + ACCOUNT_ID_MODE_DISABLED: "Q"; + ACCOUNT_ID_MODE_REQUIRED: "R"; + SIGV4A_SIGNING: "S"; + RESOLVED_ACCOUNT_ID: "T"; + FLEXIBLE_CHECKSUMS_REQ_CRC32: "U"; + FLEXIBLE_CHECKSUMS_REQ_CRC32C: "V"; + FLEXIBLE_CHECKSUMS_REQ_CRC64: "W"; + FLEXIBLE_CHECKSUMS_REQ_SHA1: "X"; + FLEXIBLE_CHECKSUMS_REQ_SHA256: "Y"; + FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED: "Z"; + FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED: "a"; + FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED: "b"; + FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED: "c"; + DDB_MAPPER: "d"; + CREDENTIALS_CODE: "e"; + // CREDENTIALS_JVM_SYSTEM_PROPERTIES: "f"; // not applicable. + CREDENTIALS_ENV_VARS: "g"; + CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN: "h"; + CREDENTIALS_STS_ASSUME_ROLE: "i"; + CREDENTIALS_STS_ASSUME_ROLE_SAML: "j"; + CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k"; + CREDENTIALS_STS_FEDERATION_TOKEN: "l"; + CREDENTIALS_STS_SESSION_TOKEN: "m"; + CREDENTIALS_PROFILE: "n"; + CREDENTIALS_PROFILE_SOURCE_PROFILE: "o"; + CREDENTIALS_PROFILE_NAMED_PROVIDER: "p"; + CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN: "q"; + CREDENTIALS_PROFILE_SSO: "r"; + CREDENTIALS_SSO: "s"; + CREDENTIALS_PROFILE_SSO_LEGACY: "t"; + CREDENTIALS_SSO_LEGACY: "u"; + CREDENTIALS_PROFILE_PROCESS: "v"; + CREDENTIALS_PROCESS: "w"; + CREDENTIALS_BOTO2_CONFIG_FILE: "x"; + CREDENTIALS_AWS_SDK_STORE: "y"; + CREDENTIALS_HTTP: "z"; + CREDENTIALS_IMDS: "0"; +}>; diff --git a/packages/util-user-agent-browser/src/index.native.spec.ts b/packages/util-user-agent-browser/src/index.native.spec.ts index 56313e41305f..b7d60589d116 100644 --- a/packages/util-user-agent-browser/src/index.native.spec.ts +++ b/packages/util-user-agent-browser/src/index.native.spec.ts @@ -6,7 +6,7 @@ it("should response basic browser default user agent", async () => { jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(undefined); const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(); expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]); - expect(userAgent[1]).toEqual(["ua", "2.0"]); + expect(userAgent[1]).toEqual(["ua", "2.1"]); expect(userAgent[2]).toEqual(["os/other"]); expect(userAgent[3]).toEqual(["lang/js"]); expect(userAgent[4]).toEqual(["md/rn"]); diff --git a/packages/util-user-agent-browser/src/index.native.ts b/packages/util-user-agent-browser/src/index.native.ts index 2d54ad3de29e..aca09d7ce7f3 100644 --- a/packages/util-user-agent-browser/src/index.native.ts +++ b/packages/util-user-agent-browser/src/index.native.ts @@ -15,7 +15,7 @@ export const defaultUserAgent = // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata ["os/other"], // language-metadata diff --git a/packages/util-user-agent-browser/src/index.spec.ts b/packages/util-user-agent-browser/src/index.spec.ts index c290ff932168..8d316d4e2d0a 100644 --- a/packages/util-user-agent-browser/src/index.spec.ts +++ b/packages/util-user-agent-browser/src/index.spec.ts @@ -7,7 +7,7 @@ it("should populate metrics", async () => { jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua); const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(); expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]); - expect(userAgent[1]).toEqual(["ua", "2.0"]); + expect(userAgent[1]).toEqual(["ua", "2.1"]); expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]); expect(userAgent[3]).toEqual(["lang/js"]); expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]); diff --git a/packages/util-user-agent-browser/src/index.ts b/packages/util-user-agent-browser/src/index.ts index 6fddd18de874..bd0d33d5b3c9 100644 --- a/packages/util-user-agent-browser/src/index.ts +++ b/packages/util-user-agent-browser/src/index.ts @@ -20,7 +20,7 @@ export const defaultUserAgent = // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata [`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version], // language-metadata diff --git a/packages/util-user-agent-node/src/index.spec.ts b/packages/util-user-agent-node/src/index.spec.ts index 07ff0a30a86e..3ffe30315a66 100644 --- a/packages/util-user-agent-node/src/index.spec.ts +++ b/packages/util-user-agent-node/src/index.spec.ts @@ -43,7 +43,7 @@ describe("defaultUserAgent", () => { const basicUserAgent: UserAgent = [ ["aws-sdk-js", "0.1.0"], - ["ua", "2.0"], + ["ua", "2.1"], ["api/s3", "0.1.0"], ["os/darwin", "19.6.0"], ["lang/js"], diff --git a/packages/util-user-agent-node/src/index.ts b/packages/util-user-agent-node/src/index.ts index f8c09d51104f..d3cec4de2cfb 100644 --- a/packages/util-user-agent-node/src/index.ts +++ b/packages/util-user-agent-node/src/index.ts @@ -31,7 +31,7 @@ export const defaultUserAgent = ({ serviceId, clientVersion }: DefaultUserAgentO // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata [`os/${platform()}`, release()], // language-metadata