UNPKG

5.05 kBPlain TextView Raw
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4import { DateUtils } from './Util';
5import {
6 presignUrl,
7 signRequest,
8 TOKEN_QUERY_PARAM,
9} from './clients/middleware/signing/signer/signatureV4';
10
11const IOT_SERVICE_NAME = 'iotdevicegateway';
12// Best practice regex to parse the service and region from an AWS endpoint
13const AWS_ENDPOINT_REGEX = /([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com(.cn)?$/;
14
15export class Signer {
16 /**
17 * Sign a HTTP request, add 'Authorization' header to request param
18 * @method sign
19 * @memberof Signer
20 * @static
21 *
22 * @param {object} request - HTTP request object
23 <pre>
24 request: {
25 method: GET | POST | PUT ...
26 url: ...,
27 headers: {
28 header1: ...
29 },
30 data: data
31 }
32 </pre>
33 * @param {object} access_info - AWS access credential info
34 <pre>
35 access_info: {
36 access_key: ...,
37 secret_key: ...,
38 session_token: ...
39 }
40 </pre>
41 * @param {object} [service_info] - AWS service type and region, optional,
42 * if not provided then parse out from url
43 <pre>
44 service_info: {
45 service: ...,
46 region: ...
47 }
48 </pre>
49 *
50 * @returns {object} Signed HTTP request
51 */
52 static sign(request, accessInfo, serviceInfo) {
53 request.headers = request.headers || {};
54
55 if (request.body && !request.data) {
56 throw new Error(
57 'The attribute "body" was found on the request object. Please use the attribute "data" instead.'
58 );
59 }
60
61 const requestToSign = {
62 ...request,
63 body: request.data,
64 url: new URL(request.url as string),
65 };
66
67 const options = getOptions(requestToSign, accessInfo, serviceInfo);
68 const signedRequest: any = signRequest(requestToSign, options);
69 // Prior to using `signRequest`, Signer accepted urls as strings and outputted urls as string. Coerce the property
70 // back to a string so as not to disrupt consumers of Signer.
71 signedRequest.url = signedRequest.url.toString();
72 // HTTP headers should be case insensitive but, to maintain parity with the previous Signer implementation and
73 // limit the impact of this implementation swap, replace lowercased headers with title cased ones.
74 signedRequest.headers.Authorization = signedRequest.headers.authorization;
75 signedRequest.headers['X-Amz-Security-Token'] =
76 signedRequest.headers['x-amz-security-token'];
77 delete signedRequest.headers.authorization;
78 delete signedRequest.headers['x-amz-security-token'];
79 return signedRequest;
80 }
81
82 static signUrl(
83 urlToSign: string,
84 accessInfo: any,
85 serviceInfo?: any,
86 expiration?: number
87 ): string;
88 static signUrl(
89 request: any,
90 accessInfo: any,
91 serviceInfo?: any,
92 expiration?: number
93 ): string;
94 static signUrl(
95 urlOrRequest: string | any,
96 accessInfo: any,
97 serviceInfo?: any,
98 expiration?: number
99 ): string {
100 const urlToSign: string =
101 typeof urlOrRequest === 'object' ? urlOrRequest.url : urlOrRequest;
102 const method: string =
103 typeof urlOrRequest === 'object' ? urlOrRequest.method : 'GET';
104 const body: any =
105 typeof urlOrRequest === 'object' ? urlOrRequest.body : undefined;
106
107 const presignable = {
108 body,
109 method,
110 url: new URL(urlToSign),
111 };
112
113 const options = getOptions(
114 presignable,
115 accessInfo,
116 serviceInfo,
117 expiration
118 );
119 const signedUrl = presignUrl(presignable, options);
120 if (
121 accessInfo.session_token &&
122 !sessionTokenRequiredInSigning(options.signingService)
123 ) {
124 signedUrl.searchParams.append(
125 TOKEN_QUERY_PARAM,
126 accessInfo.session_token
127 );
128 }
129 return signedUrl.toString();
130 }
131}
132
133const getOptions = (request, accessInfo, serviceInfo, expiration?) => {
134 const { access_key, secret_key, session_token } = accessInfo ?? {};
135 const { region: urlRegion, service: urlService } = parseServiceInfo(
136 request.url
137 );
138 const { region = urlRegion, service = urlService } = serviceInfo ?? {};
139 const credentials = {
140 accessKeyId: access_key,
141 secretAccessKey: secret_key,
142 ...(sessionTokenRequiredInSigning(service)
143 ? { sessionToken: session_token }
144 : {}),
145 };
146 return {
147 credentials,
148 signingDate: DateUtils.getDateWithClockOffset(),
149 signingRegion: region,
150 signingService: service,
151 ...(expiration && { expiration }),
152 };
153};
154
155// TODO: V6 investigate whether add to custom clients' general signer implementation.
156const parseServiceInfo = (url: URL) => {
157 const host = url.host;
158 const matched = host.match(AWS_ENDPOINT_REGEX) ?? [];
159 let parsed = matched.slice(1, 3);
160
161 if (parsed[1] === 'es') {
162 // Elastic Search
163 parsed = parsed.reverse();
164 }
165
166 return {
167 service: parsed[0],
168 region: parsed[1],
169 };
170};
171
172// IoT service does not allow the session token in the canonical request
173// https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
174// TODO: V6 investigate whether add to custom clients' general signer implementation.
175const sessionTokenRequiredInSigning = (service: string) =>
176 service !== IOT_SERVICE_NAME;