Skip to content

Commit 88baaad

Browse files
authored
feat(s3-request-presigner): add getSignedUrl() from client and commands (#1454)
* feat(s3-request-presigner): add getSignedUrl() from client and commands
1 parent f95cce3 commit 88baaad

File tree

7 files changed

+244
-47
lines changed

7 files changed

+244
-47
lines changed

Diff for: packages/s3-request-presigner/README.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,41 @@
33
[![NPM version](https://img.shields.io/npm/v/@aws-sdk/s3-request-presigner/beta.svg)](https://www.npmjs.com/package/@aws-sdk/s3-request-presigner)
44
[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/s3-request-presigner/beta.svg)](https://www.npmjs.com/package/@aws-sdk/s3-request-presigner)
55

6-
This package provides a presigner based on signature V4 that will attempt to generate signed url for S3.
6+
This package provides a presigner based on signature V4 that will attempt to
7+
generate signed url for S3.
8+
9+
### Get Presigned URL with Client and Command
10+
11+
You can generated presigned url from S3 client and command. Here's the example:
12+
13+
JavaScript Example:
14+
15+
```javascript
16+
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
17+
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
18+
const client = new S3Client(clientParams);
19+
const command = new GetObjectCommand(getObjectParams);
20+
const url = await getSignedUrl(client, command, { expiresIn: 3600 });
21+
```
22+
23+
ES6 Example
24+
25+
```javascript
26+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
27+
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
28+
const client = new S3Client(clientParams);
29+
const command = new GetObjectCommand(getObjectParams);
30+
const url = await getSignedUrl(client, command, { expiresIn: 3600 });
31+
```
32+
33+
You can get signed URL for other S3 operations too, like `PutObjectCommand`.
34+
`expiresIn` config from the examples above is optional. If not set, it's default
35+
at `900`.
36+
37+
If you already have a request, you can pre-sign the request following the
38+
section bellow.
39+
40+
### Get Presigned URL from an Existing Request
741

842
JavaScript Example:
943

Diff for: packages/s3-request-presigner/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818
},
1919
"license": "Apache-2.0",
2020
"dependencies": {
21+
"@aws-sdk/protocol-http": "1.0.0-gamma.5",
2122
"@aws-sdk/signature-v4": "1.0.0-gamma.5",
23+
"@aws-sdk/smithy-client": "1.0.0-gamma.5",
2224
"@aws-sdk/types": "1.0.0-gamma.4",
2325
"@aws-sdk/util-create-request": "1.0.0-gamma.5",
2426
"@aws-sdk/util-format-url": "1.0.0-gamma.5",
2527
"tslib": "^1.8.0"
2628
},
2729
"devDependencies": {
2830
"@aws-sdk/hash-node": "1.0.0-gamma.5",
29-
"@aws-sdk/protocol-http": "1.0.0-gamma.5",
31+
"@aws-sdk/client-s3": "1.0.0-gamma.6",
3032
"@types/jest": "^26.0.4",
3133
"@types/node": "^12.0.2",
3234
"jest": "^26.1.0",
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const mockV4Sign = jest.fn();
2+
const mockV4Presign = jest.fn();
3+
const mockV4 = jest.fn().mockReturnValue({
4+
presign: mockV4Presign,
5+
sign: mockV4Sign,
6+
});
7+
jest.mock("@aws-sdk/signature-v4", () => ({
8+
SignatureV4: mockV4,
9+
}));
10+
11+
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
12+
13+
const mockPresign = jest.fn();
14+
const mockPresigner = jest.fn().mockReturnValue({
15+
presign: mockPresign,
16+
});
17+
jest.mock("./presigner", () => ({
18+
S3RequestPresigner: mockPresigner,
19+
}));
20+
jest.mock("@aws-sdk/util-format-url", () => ({
21+
formatUrl: (url: any) => url,
22+
}));
23+
24+
import { RequestPresigningArguments } from "@aws-sdk/types/src";
25+
26+
import { getSignedUrl } from "./getSignedUrl";
27+
28+
describe("getSignedUrl", () => {
29+
beforeEach(() => {
30+
mockPresign.mockReset();
31+
});
32+
33+
it("should call S3Presigner.sign", async () => {
34+
const mockPresigned = "a presigned url";
35+
mockPresign.mockReturnValue(mockPresigned);
36+
const client = new S3Client({});
37+
const command = new GetObjectCommand({
38+
Bucket: "Bucket",
39+
Key: "Key",
40+
});
41+
const signed = await getSignedUrl(client, command);
42+
expect(signed).toBe(mockPresigned);
43+
expect(mockPresign).toBeCalled();
44+
expect(mockV4Presign).not.toBeCalled();
45+
expect(mockV4Sign).not.toBeCalled();
46+
// do not add extra middleware to the client or command
47+
expect(client.middlewareStack.remove("presignInterceptMiddleware")).toBe(false);
48+
expect(command.middlewareStack.remove("presignInterceptMiddleware")).toBe(false);
49+
});
50+
51+
it("should presign with signing region and service in context if exists", async () => {
52+
const mockPresigned = "a presigned url";
53+
mockPresign.mockReturnValue(mockPresigned);
54+
const signingRegion = "aws-foo-1";
55+
const signingService = "bar";
56+
const client = new S3Client({});
57+
client.middlewareStack.addRelativeTo(
58+
(next: any, context: any) => (args: any) => {
59+
context["signing_region"] = signingRegion;
60+
context["signing_service"] = signingService;
61+
return next(args);
62+
},
63+
{
64+
relation: "before",
65+
toMiddleware: "presignInterceptMiddleware",
66+
}
67+
);
68+
const command = new GetObjectCommand({
69+
Bucket: "Bucket",
70+
Key: "Key",
71+
});
72+
await getSignedUrl(client, command);
73+
expect(mockPresign).toBeCalled();
74+
expect(mockPresign.mock.calls[0][1]).toMatchObject({
75+
signingRegion,
76+
signingService,
77+
});
78+
});
79+
80+
it("should presign with parameters from presign options if set", async () => {
81+
const mockPresigned = "a presigned url";
82+
mockPresign.mockReturnValue(mockPresigned);
83+
const options: RequestPresigningArguments = {
84+
signingRegion: "aws-foo-1",
85+
signingService: "bar",
86+
expiresIn: 900,
87+
signingDate: new Date(),
88+
signableHeaders: new Set(["head-1", "head-2"]),
89+
unsignableHeaders: new Set(["head-3", "head-4"]),
90+
};
91+
const client = new S3Client({});
92+
const command = new GetObjectCommand({
93+
Bucket: "Bucket",
94+
Key: "Key",
95+
});
96+
await getSignedUrl(client, command, options);
97+
expect(mockPresign).toBeCalled();
98+
expect(mockPresign.mock.calls[0][1]).toMatchObject(options);
99+
});
100+
});

Diff for: packages/s3-request-presigner/src/getSignedUrl.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { HttpRequest } from "@aws-sdk/protocol-http";
2+
import { Client, Command } from "@aws-sdk/smithy-client";
3+
import { BuildMiddleware, MetadataBearer, RequestPresigningArguments } from "@aws-sdk/types";
4+
import { formatUrl } from "@aws-sdk/util-format-url";
5+
6+
import { S3RequestPresigner } from "./presigner";
7+
8+
export const getSignedUrl = async <
9+
InputTypesUnion extends object,
10+
InputType extends InputTypesUnion,
11+
OutputType extends MetadataBearer = MetadataBearer
12+
>(
13+
client: Client<any, InputTypesUnion, MetadataBearer, any>,
14+
command: Command<InputType, OutputType, any, InputTypesUnion, MetadataBearer>,
15+
options: RequestPresigningArguments = {}
16+
): Promise<string> => {
17+
const s3Presigner = new S3RequestPresigner({ ...client.config });
18+
const presignInterceptMiddleware: BuildMiddleware<InputTypesUnion, MetadataBearer> = (next, context) => async (
19+
args
20+
) => {
21+
const { request } = args;
22+
if (!HttpRequest.isInstance(request)) {
23+
throw new Error("Request to be presigned is not an valid HTTP request.");
24+
}
25+
// Retry information headers are not meaningful in presigned URLs
26+
delete request.headers["amz-sdk-invocation-id"];
27+
delete request.headers["amz-sdk-request"];
28+
29+
const presigned = await s3Presigner.presign(request, {
30+
...options,
31+
signingRegion: options.signingRegion ?? context["signing_region"],
32+
signingService: options.signingService ?? context["signing_service"],
33+
});
34+
return {
35+
// Intercept the middleware stack by returning fake response
36+
response: {},
37+
output: {
38+
$metadata: { httpStatusCode: 200 },
39+
presigned,
40+
},
41+
} as any;
42+
};
43+
client.middlewareStack.addRelativeTo(presignInterceptMiddleware, {
44+
name: "presignInterceptMiddleware",
45+
relation: "before",
46+
toMiddleware: "awsAuthMiddleware",
47+
});
48+
49+
let presigned: HttpRequest;
50+
try {
51+
const output = await client.send(command);
52+
//@ts-ignore the output is faked, so it's not actually OutputType
53+
presigned = output.presigned;
54+
} finally {
55+
client.middlewareStack.remove("presignInterceptMiddleware");
56+
}
57+
58+
return formatUrl(presigned);
59+
};

Diff for: packages/s3-request-presigner/src/index.ts

+2-44
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,2 @@
1-
import { SignatureV4, SignatureV4CryptoInit, SignatureV4Init } from "@aws-sdk/signature-v4";
2-
import { RequestPresigner, RequestPresigningArguments } from "@aws-sdk/types";
3-
import { HttpRequest as IHttpRequest } from "@aws-sdk/types";
4-
5-
import { SHA256_HEADER, UNSIGNED_PAYLOAD } from "./constants";
6-
7-
/**
8-
* PartialBy<T, K> makes properties specified in K optional in interface T
9-
* see: https://stackoverflow.com/questions/43159887/make-a-single-property-optional-in-typescript
10-
* */
11-
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
12-
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
13-
14-
export type S3RequestPresignerOptions = PartialBy<
15-
SignatureV4Init & SignatureV4CryptoInit,
16-
"service" | "uriEscapePath"
17-
> & { signingName?: string };
18-
19-
export class S3RequestPresigner implements RequestPresigner {
20-
private readonly signer: SignatureV4;
21-
constructor(options: S3RequestPresignerOptions) {
22-
const resolvedOptions = {
23-
// Allow `signingName` because we want to support usecase of supply client's resolved config
24-
// directly. Where service equals signingName.
25-
service: options.signingName || options.service || "s3",
26-
uriEscapePath: options.uriEscapePath || false,
27-
...options,
28-
};
29-
this.signer = new SignatureV4(resolvedOptions);
30-
}
31-
32-
public async presign(
33-
requestToSign: IHttpRequest,
34-
{ unsignableHeaders = new Set(), ...options }: RequestPresigningArguments = {}
35-
): Promise<IHttpRequest> {
36-
unsignableHeaders.add("content-type");
37-
requestToSign.headers[SHA256_HEADER] = UNSIGNED_PAYLOAD;
38-
return this.signer.presign(requestToSign, {
39-
expiresIn: 900,
40-
unsignableHeaders,
41-
...options,
42-
});
43-
}
44-
}
1+
export * from "./presigner";
2+
export * from "./getSignedUrl";

Diff for: packages/s3-request-presigner/src/index.spec.ts renamed to packages/s3-request-presigner/src/presigner.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
SIGNED_HEADERS_QUERY_PARAM,
1313
UNSIGNED_PAYLOAD,
1414
} from "./constants";
15-
import { S3RequestPresigner, S3RequestPresignerOptions } from "./index";
15+
import { S3RequestPresigner, S3RequestPresignerOptions } from "./presigner";
1616

1717
describe("s3 presigner", () => {
1818
const s3ResolvedConfig: S3RequestPresignerOptions = {

Diff for: packages/s3-request-presigner/src/presigner.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { SignatureV4, SignatureV4CryptoInit, SignatureV4Init } from "@aws-sdk/signature-v4";
2+
import { RequestPresigner, RequestPresigningArguments } from "@aws-sdk/types";
3+
import { HttpRequest as IHttpRequest } from "@aws-sdk/types";
4+
5+
import { SHA256_HEADER, UNSIGNED_PAYLOAD } from "./constants";
6+
7+
/**
8+
* PartialBy<T, K> makes properties specified in K optional in interface T
9+
* see: https://stackoverflow.com/questions/43159887/make-a-single-property-optional-in-typescript
10+
* */
11+
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
12+
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
13+
14+
export type S3RequestPresignerOptions = PartialBy<
15+
SignatureV4Init & SignatureV4CryptoInit,
16+
"service" | "uriEscapePath"
17+
> & { signingName?: string };
18+
19+
export class S3RequestPresigner implements RequestPresigner {
20+
private readonly signer: SignatureV4;
21+
constructor(options: S3RequestPresignerOptions) {
22+
const resolvedOptions = {
23+
// Allow `signingName` because we want to support usecase of supply client's resolved config
24+
// directly. Where service equals signingName.
25+
service: options.signingName || options.service || "s3",
26+
uriEscapePath: options.uriEscapePath || false,
27+
...options,
28+
};
29+
this.signer = new SignatureV4(resolvedOptions);
30+
}
31+
32+
public async presign(
33+
requestToSign: IHttpRequest,
34+
{ unsignableHeaders = new Set(), ...options }: RequestPresigningArguments = {}
35+
): Promise<IHttpRequest> {
36+
unsignableHeaders.add("content-type");
37+
requestToSign.headers[SHA256_HEADER] = UNSIGNED_PAYLOAD;
38+
return this.signer.presign(requestToSign, {
39+
expiresIn: 900,
40+
unsignableHeaders,
41+
...options,
42+
});
43+
}
44+
}

0 commit comments

Comments
 (0)