Skip to content

Commit 30cfb43

Browse files
committed
feat(s3-request-presigner): add getSignedUrl() from client and commands
1 parent 8a0febd commit 30cfb43

File tree

7 files changed

+201
-3
lines changed

7 files changed

+201
-3
lines changed

packages/s3-request-presigner/README.md

Lines changed: 35 additions & 1 deletion
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(construcParams);
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(construcParams);
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

packages/s3-request-presigner/package.json

Lines changed: 3 additions & 1 deletion
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",
Lines changed: 100 additions & 0 deletions
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+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 middleware stack should not be altered by this function
56+
client.middlewareStack.remove("presignInterceptMiddleware");
57+
}
58+
59+
return formatUrl(presigned);
60+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./presigner";
2+
export * from "./getSignedUrl";

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

Lines changed: 1 addition & 1 deletion
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 = {

0 commit comments

Comments
 (0)