Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(middleware-user-agent): update to user agent 2.1 spec #6536

Merged
merged 1 commit into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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("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" },
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;
};
}
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
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";
Loading