diff --git a/packages/core/package.json b/packages/core/package.json index a83524622de9..43d3e40cb6a0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -79,6 +79,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-sdk/types": "*", "@smithy/core": "^2.4.7", "@smithy/node-config-provider": "^3.1.8", "@smithy/property-provider": "^3.1.7", diff --git a/packages/core/src/submodules/client/index.ts b/packages/core/src/submodules/client/index.ts index ed9af92686a2..2c463d5f978f 100644 --- a/packages/core/src/submodules/client/index.ts +++ b/packages/core/src/submodules/client/index.ts @@ -1 +1,2 @@ export * from "./emitWarningIfUnsupportedVersion"; +export * from "./setFeature"; diff --git a/packages/core/src/submodules/client/setFeature.spec.ts b/packages/core/src/submodules/client/setFeature.spec.ts new file mode 100644 index 000000000000..729867ce7d97 --- /dev/null +++ b/packages/core/src/submodules/client/setFeature.spec.ts @@ -0,0 +1,10 @@ +import { AwsHandlerExecutionContext } from "@aws-sdk/types"; + +import { setFeature } from "./setFeature"; + +describe(setFeature.name, () => { + it("creates the context object path if needed", () => { + const context: AwsHandlerExecutionContext = {}; + setFeature(context, "ACCOUNT_ID_ENDPOINT", "O"); + }); +}); diff --git a/packages/core/src/submodules/client/setFeature.ts b/packages/core/src/submodules/client/setFeature.ts new file mode 100644 index 000000000000..01f0a436fca5 --- /dev/null +++ b/packages/core/src/submodules/client/setFeature.ts @@ -0,0 +1,26 @@ +import type { AwsHandlerExecutionContext, AwsSdkFeatures } from "@aws-sdk/types"; + +/** + * @internal + * Indicates to the request context that a given feature is active. + * + * @param context - handler execution context. + * @param feature - readable name of feature. + * @param value - encoding value of feature. This is required because the + * specification asks the SDK not to include a runtime lookup of all + * the feature identifiers. + */ +export function setFeature( + context: AwsHandlerExecutionContext, + feature: F, + value: AwsSdkFeatures[F] +) { + if (!context.__aws_sdk_context) { + context.__aws_sdk_context = { + features: {}, + }; + } else if (!context.__aws_sdk_context.features) { + context.__aws_sdk_context.features = {}; + } + context.__aws_sdk_context.features![feature] = value; +} diff --git a/packages/middleware-user-agent/src/encode-features.spec.ts b/packages/middleware-user-agent/src/encode-features.spec.ts new file mode 100644 index 000000000000..c9b01a0eb166 --- /dev/null +++ b/packages/middleware-user-agent/src/encode-features.spec.ts @@ -0,0 +1,26 @@ +import { encodeFeatures } from "./encode-features"; + +describe(encodeFeatures.name, () => { + it("encodes empty features", () => { + expect(encodeFeatures({})).toEqual(""); + }); + + it("encodes features", () => { + expect( + encodeFeatures({ + A: "A", + z: "z", + } as any) + ).toEqual("A,z"); + }); + + it("drops values that would exceed 1024 bytes", () => { + expect( + encodeFeatures({ + 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-features.ts b/packages/middleware-user-agent/src/encode-features.ts new file mode 100644 index 000000000000..e1fc88c458d4 --- /dev/null +++ b/packages/middleware-user-agent/src/encode-features.ts @@ -0,0 +1,28 @@ +import type { AwsSdkFeatures } from "@aws-sdk/types"; + +const BYTE_LIMIT = 1024; + +/** + * @internal + */ +export function encodeFeatures(features: AwsSdkFeatures): string { + let buffer = ""; + + // currently all possible values are 1 byte, + // so string length is used. + + for (const key in features) { + const val = features[key as keyof typeof features]!; + 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..aa2c67fef18c 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 3ea4d832f638..b01e90b967ea 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts @@ -89,6 +89,38 @@ describe("userAgentMiddleware", () => { expect(sdkUserAgent).toEqual(expect.stringContaining("custom_ua/abc")); }); + describe("features", () => { + it("should collect features from the context", async () => { + const middleware = userAgentMiddleware({ + defaultUserAgentProvider: async () => [ + ["default_agent", "1.0.0"], + ["aws-sdk-js", "1.0.0"], + ], + runtime: "node", + userAgentAppId: async () => undefined, + }); + + 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 83fd46f26420..260151a13cb5 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 { encodeFeatures } from "./encode-features"; /** * Build user agent header sections from: @@ -39,14 +41,22 @@ 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) || []; - let defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent); + const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent); + const awsContext = context as AwsHandlerExecutionContext; + defaultUserAgent.push( + `m/${encodeFeatures( + Object.assign({}, context.__smithy_context?.features, awsContext.__aws_sdk_context?.features) + )}` + ); const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || []; const appId = await options.userAgentAppId(); if (appId) { diff --git a/packages/types/src/feature-ids.ts b/packages/types/src/feature-ids.ts new file mode 100644 index 000000000000..7dc1b9078aed --- /dev/null +++ b/packages/types/src/feature-ids.ts @@ -0,0 +1,58 @@ +/** + * @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/types/src/index.ts b/packages/types/src/index.ts index ab64cd947b8c..ce32277d0364 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -12,6 +12,7 @@ export * from "./encode"; export * from "./endpoint"; export * from "./eventStream"; export * from "./extensions"; +export * from "./feature-ids"; export * from "./http"; export * from "./identity"; export * from "./logger"; diff --git a/packages/types/src/middleware.ts b/packages/types/src/middleware.ts index 3ae51bd9a83d..9cdc79fe2e60 100644 --- a/packages/types/src/middleware.ts +++ b/packages/types/src/middleware.ts @@ -1,3 +1,7 @@ +import { HandlerExecutionContext } from "@smithy/types"; + +import { AwsSdkFeatures } from "./feature-ids"; + export { AbsoluteLocation, BuildHandler, @@ -38,3 +42,14 @@ 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; + }; +} 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 5454ca3f2e22..a0ed2582bf58 100644 --- a/packages/util-user-agent-browser/src/index.spec.ts +++ b/packages/util-user-agent-browser/src/index.spec.ts @@ -19,7 +19,7 @@ describe("defaultUserAgent", () => { it("should populate metrics", async () => { const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig); 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"]); @@ -47,4 +47,4 @@ describe("defaultUserAgent", () => { expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"])); expect(userAgent.length).toBe(6); }); -}); \ No newline at end of file +}); diff --git a/packages/util-user-agent-browser/src/index.ts b/packages/util-user-agent-browser/src/index.ts index 1f59b7f1a1bc..5153db33cf3d 100644 --- a/packages/util-user-agent-browser/src/index.ts +++ b/packages/util-user-agent-browser/src/index.ts @@ -24,7 +24,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/defaultUserAgent.spec.ts b/packages/util-user-agent-node/src/defaultUserAgent.spec.ts index 1be9bee84b28..86c662588306 100644 --- a/packages/util-user-agent-node/src/defaultUserAgent.spec.ts +++ b/packages/util-user-agent-node/src/defaultUserAgent.spec.ts @@ -39,7 +39,7 @@ describe("createDefaultUserAgentProvider", () => { 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/defaultUserAgent.ts b/packages/util-user-agent-node/src/defaultUserAgent.ts index 12d371ccc4d8..df23988414c2 100644 --- a/packages/util-user-agent-node/src/defaultUserAgent.ts +++ b/packages/util-user-agent-node/src/defaultUserAgent.ts @@ -26,7 +26,7 @@ export const createDefaultUserAgentProvider = ({ serviceId, clientVersion }: Def // sdk-metadata ["aws-sdk-js", clientVersion], // ua-metadata - ["ua", "2.0"], + ["ua", "2.1"], // os-metadata [`os/${platform()}`, release()], // language-metadata diff --git a/packages/util-user-agent-node/src/index.ts b/packages/util-user-agent-node/src/index.ts index 8aeee67f65c7..cbf37f23bbca 100644 --- a/packages/util-user-agent-node/src/index.ts +++ b/packages/util-user-agent-node/src/index.ts @@ -1,2 +1,2 @@ export * from "./defaultUserAgent"; -export * from "./nodeAppIdConfigOptions"; \ No newline at end of file +export * from "./nodeAppIdConfigOptions";