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 Sep 30, 2024
1 parent be5d9f0 commit 8220029
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 9 deletions.
26 changes: 26 additions & 0 deletions packages/middleware-user-agent/src/encode-metrics.spec.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
28 changes: 28 additions & 0 deletions packages/middleware-user-agent/src/encode-metrics.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 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;
}
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
31 changes: 31 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 @@ -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" },
Expand Down
10 changes: 8 additions & 2 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 { encodeMetrics } from "./encode-metrics";

/**
* Build user agent header sections from:
Expand All @@ -39,14 +41,18 @@ 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) || [];
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();

Expand Down
72 changes: 72 additions & 0 deletions packages/types/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { HandlerExecutionContext } from "@smithy/types";

export {
AbsoluteLocation,
BuildHandler,
Expand Down Expand Up @@ -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";
}>;
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
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
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 @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-node/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
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
Expand Up @@ -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
Expand Down

0 comments on commit 8220029

Please sign in to comment.