Skip to content

Commit

Permalink
chore(middleware-user-agent): update to user agent 2.1 spec
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed Oct 3, 2024
1 parent 2a50045 commit b9e2e90
Show file tree
Hide file tree
Showing 19 changed files with 220 additions and 12 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/submodules/client/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./emitWarningIfUnsupportedVersion";
export * from "./setFeature";
10 changes: 10 additions & 0 deletions packages/core/src/submodules/client/setFeature.spec.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
26 changes: 26 additions & 0 deletions packages/core/src/submodules/client/setFeature.ts
Original file line number Diff line number Diff line change
@@ -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<F extends keyof AwsSdkFeatures>(
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;
}
26 changes: 26 additions & 0 deletions packages/middleware-user-agent/src/encode-features.spec.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
28 changes: 28 additions & 0 deletions packages/middleware-user-agent/src/encode-features.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
32 changes: 32 additions & 0 deletions packages/middleware-user-agent/src/user-agent-middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,38 @@ describe("userAgentMiddleware", () => {
expect(sdkUserAgent).toEqual(expect.stringContaining("custom_ua/abc"));
});

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",
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" },
Expand Down
16 changes: 13 additions & 3 deletions packages/middleware-user-agent/src/user-agent-middleware.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,6 +23,7 @@ import {
USER_AGENT,
X_AMZ_USER_AGENT,
} from "./constants";
import { encodeFeatures } from "./encode-features";

/**
* Build user agent header sections from:
Expand All @@ -39,14 +41,22 @@ export const userAgentMiddleware =
(options: UserAgentResolvedConfig) =>
<Output extends MetadataBearer>(
next: BuildHandler<any, any>,
context: HandlerExecutionContext
context: HandlerExecutionContext | AwsHandlerExecutionContext
): BuildHandler<any, any> =>
async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
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) {
Expand Down
58 changes: 58 additions & 0 deletions packages/types/src/feature-ids.ts
Original file line number Diff line number Diff line change
@@ -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";
}>;
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
15 changes: 15 additions & 0 deletions packages/types/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { HandlerExecutionContext } from "@smithy/types";

import { AwsSdkFeatures } from "./feature-ids";

export {
AbsoluteLocation,
BuildHandler,
Expand Down Expand Up @@ -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;
};
}
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.native.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/util-user-agent-browser/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down Expand Up @@ -47,4 +47,4 @@ describe("defaultUserAgent", () => {
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
expect(userAgent.length).toBe(6);
});
});
});
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-node/src/defaultUserAgent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-node/src/defaultUserAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./defaultUserAgent";
export * from "./nodeAppIdConfigOptions";
export * from "./nodeAppIdConfigOptions";

0 comments on commit b9e2e90

Please sign in to comment.