UNPKG

13.9 kBJavaScriptView Raw
1"use strict";
2// Copyright 2020 Google LLC
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16 if (k2 === undefined) k2 = k;
17 var desc = Object.getOwnPropertyDescriptor(m, k);
18 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19 desc = { enumerable: true, get: function() { return m[k]; } };
20 }
21 Object.defineProperty(o, k2, desc);
22}) : (function(o, m, k, k2) {
23 if (k2 === undefined) k2 = k;
24 o[k2] = m[k];
25}));
26var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27 Object.defineProperty(o, "default", { enumerable: true, value: v });
28}) : function(o, v) {
29 o["default"] = v;
30});
31var __importStar = (this && this.__importStar) || function (mod) {
32 if (mod && mod.__esModule) return mod;
33 var result = {};
34 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
35 __setModuleDefault(result, mod);
36 return result;
37};
38Object.defineProperty(exports, "__esModule", { value: true });
39exports.SigningError = exports.URLSigner = exports.PATH_STYLED_HOST = exports.SignerExceptionMessages = void 0;
40const crypto = __importStar(require("crypto"));
41const url = __importStar(require("url"));
42const storage_js_1 = require("./storage.js");
43const util_js_1 = require("./util.js");
44var SignerExceptionMessages;
45(function (SignerExceptionMessages) {
46 SignerExceptionMessages["ACCESSIBLE_DATE_INVALID"] = "The accessible at date provided was invalid.";
47 SignerExceptionMessages["EXPIRATION_BEFORE_ACCESSIBLE_DATE"] = "An expiration date cannot be before accessible date.";
48 SignerExceptionMessages["X_GOOG_CONTENT_SHA256"] = "The header X-Goog-Content-SHA256 must be a hexadecimal string.";
49})(SignerExceptionMessages || (exports.SignerExceptionMessages = SignerExceptionMessages = {}));
50/*
51 * Default signing version for getSignedUrl is 'v2'.
52 */
53const DEFAULT_SIGNING_VERSION = 'v2';
54const SEVEN_DAYS = 7 * 24 * 60 * 60;
55/**
56 * @const {string}
57 * @deprecated - unused
58 */
59exports.PATH_STYLED_HOST = 'https://storage.googleapis.com';
60class URLSigner {
61 constructor(auth, bucket, file,
62 /**
63 * A {@link Storage} object.
64 *
65 * @privateRemarks
66 *
67 * Technically this is a required field, however it would be a breaking change to
68 * move it before optional properties. In the next major we should refactor the
69 * constructor of this class to only accept a config object.
70 */
71 storage = new storage_js_1.Storage()) {
72 this.auth = auth;
73 this.bucket = bucket;
74 this.file = file;
75 this.storage = storage;
76 }
77 getSignedUrl(cfg) {
78 const expiresInSeconds = this.parseExpires(cfg.expires);
79 const method = cfg.method;
80 const accessibleAtInSeconds = this.parseAccessibleAt(cfg.accessibleAt);
81 if (expiresInSeconds < accessibleAtInSeconds) {
82 throw new Error(SignerExceptionMessages.EXPIRATION_BEFORE_ACCESSIBLE_DATE);
83 }
84 let customHost;
85 // Default style is `path`.
86 const isVirtualHostedStyle = cfg.virtualHostedStyle || false;
87 if (cfg.cname) {
88 customHost = cfg.cname;
89 }
90 else if (isVirtualHostedStyle) {
91 customHost = `https://${this.bucket.name}.storage.${this.storage.universeDomain}`;
92 }
93 const secondsToMilliseconds = 1000;
94 const config = Object.assign({}, cfg, {
95 method,
96 expiration: expiresInSeconds,
97 accessibleAt: new Date(secondsToMilliseconds * accessibleAtInSeconds),
98 bucket: this.bucket.name,
99 file: this.file ? (0, util_js_1.encodeURI)(this.file.name, false) : undefined,
100 });
101 if (customHost) {
102 config.cname = customHost;
103 }
104 const version = cfg.version || DEFAULT_SIGNING_VERSION;
105 let promise;
106 if (version === 'v2') {
107 promise = this.getSignedUrlV2(config);
108 }
109 else if (version === 'v4') {
110 promise = this.getSignedUrlV4(config);
111 }
112 else {
113 throw new Error(`Invalid signed URL version: ${version}. Supported versions are 'v2' and 'v4'.`);
114 }
115 return promise.then(query => {
116 var _a;
117 query = Object.assign(query, cfg.queryParams);
118 const signedUrl = new url.URL(((_a = cfg.host) === null || _a === void 0 ? void 0 : _a.toString()) || config.cname || this.storage.apiEndpoint);
119 signedUrl.pathname = this.getResourcePath(!!config.cname, this.bucket.name, config.file);
120 // eslint-disable-next-line @typescript-eslint/no-explicit-any
121 signedUrl.search = (0, util_js_1.qsStringify)(query);
122 return signedUrl.href;
123 });
124 }
125 getSignedUrlV2(config) {
126 const canonicalHeadersString = this.getCanonicalHeaders(config.extensionHeaders || {});
127 const resourcePath = this.getResourcePath(false, config.bucket, config.file);
128 const blobToSign = [
129 config.method,
130 config.contentMd5 || '',
131 config.contentType || '',
132 config.expiration,
133 canonicalHeadersString + resourcePath,
134 ].join('\n');
135 const sign = async () => {
136 var _a;
137 const auth = this.auth;
138 try {
139 const signature = await auth.sign(blobToSign, (_a = config.signingEndpoint) === null || _a === void 0 ? void 0 : _a.toString());
140 const credentials = await auth.getCredentials();
141 return {
142 GoogleAccessId: credentials.client_email,
143 Expires: config.expiration,
144 Signature: signature,
145 };
146 }
147 catch (err) {
148 const error = err;
149 const signingErr = new SigningError(error.message);
150 signingErr.stack = error.stack;
151 throw signingErr;
152 }
153 };
154 return sign();
155 }
156 getSignedUrlV4(config) {
157 var _a;
158 config.accessibleAt = config.accessibleAt
159 ? config.accessibleAt
160 : new Date();
161 const millisecondsToSeconds = 1.0 / 1000.0;
162 const expiresPeriodInSeconds = config.expiration - config.accessibleAt.valueOf() * millisecondsToSeconds;
163 // v4 limit expiration to be 7 days maximum
164 if (expiresPeriodInSeconds > SEVEN_DAYS) {
165 throw new Error(`Max allowed expiration is seven days (${SEVEN_DAYS} seconds).`);
166 }
167 const extensionHeaders = Object.assign({}, config.extensionHeaders);
168 const fqdn = new url.URL(((_a = config.host) === null || _a === void 0 ? void 0 : _a.toString()) || config.cname || this.storage.apiEndpoint);
169 extensionHeaders.host = fqdn.hostname;
170 if (config.contentMd5) {
171 extensionHeaders['content-md5'] = config.contentMd5;
172 }
173 if (config.contentType) {
174 extensionHeaders['content-type'] = config.contentType;
175 }
176 let contentSha256;
177 const sha256Header = extensionHeaders['x-goog-content-sha256'];
178 if (sha256Header) {
179 if (typeof sha256Header !== 'string' ||
180 !/[A-Fa-f0-9]{40}/.test(sha256Header)) {
181 throw new Error(SignerExceptionMessages.X_GOOG_CONTENT_SHA256);
182 }
183 contentSha256 = sha256Header;
184 }
185 const signedHeaders = Object.keys(extensionHeaders)
186 .map(header => header.toLowerCase())
187 .sort()
188 .join(';');
189 const extensionHeadersString = this.getCanonicalHeaders(extensionHeaders);
190 const datestamp = (0, util_js_1.formatAsUTCISO)(config.accessibleAt);
191 const credentialScope = `${datestamp}/auto/storage/goog4_request`;
192 const sign = async () => {
193 var _a;
194 const credentials = await this.auth.getCredentials();
195 const credential = `${credentials.client_email}/${credentialScope}`;
196 const dateISO = (0, util_js_1.formatAsUTCISO)(config.accessibleAt ? config.accessibleAt : new Date(), true);
197 const queryParams = {
198 'X-Goog-Algorithm': 'GOOG4-RSA-SHA256',
199 'X-Goog-Credential': credential,
200 'X-Goog-Date': dateISO,
201 'X-Goog-Expires': expiresPeriodInSeconds.toString(10),
202 'X-Goog-SignedHeaders': signedHeaders,
203 ...(config.queryParams || {}),
204 };
205 // eslint-disable-next-line @typescript-eslint/no-explicit-any
206 const canonicalQueryParams = this.getCanonicalQueryParams(queryParams);
207 const canonicalRequest = this.getCanonicalRequest(config.method, this.getResourcePath(!!config.cname, config.bucket, config.file), canonicalQueryParams, extensionHeadersString, signedHeaders, contentSha256);
208 const hash = crypto
209 .createHash('sha256')
210 .update(canonicalRequest)
211 .digest('hex');
212 const blobToSign = [
213 'GOOG4-RSA-SHA256',
214 dateISO,
215 credentialScope,
216 hash,
217 ].join('\n');
218 try {
219 const signature = await this.auth.sign(blobToSign, (_a = config.signingEndpoint) === null || _a === void 0 ? void 0 : _a.toString());
220 const signatureHex = Buffer.from(signature, 'base64').toString('hex');
221 const signedQuery = Object.assign({}, queryParams, {
222 'X-Goog-Signature': signatureHex,
223 });
224 return signedQuery;
225 }
226 catch (err) {
227 const error = err;
228 const signingErr = new SigningError(error.message);
229 signingErr.stack = error.stack;
230 throw signingErr;
231 }
232 };
233 return sign();
234 }
235 /**
236 * Create canonical headers for signing v4 url.
237 *
238 * The canonical headers for v4-signing a request demands header names are
239 * first lowercased, followed by sorting the header names.
240 * Then, construct the canonical headers part of the request:
241 * <lowercasedHeaderName> + ":" + Trim(<value>) + "\n"
242 * ..
243 * <lowercasedHeaderName> + ":" + Trim(<value>) + "\n"
244 *
245 * @param headers
246 * @private
247 */
248 getCanonicalHeaders(headers) {
249 // Sort headers by their lowercased names
250 const sortedHeaders = (0, util_js_1.objectEntries)(headers)
251 // Convert header names to lowercase
252 .map(([headerName, value]) => [
253 headerName.toLowerCase(),
254 value,
255 ])
256 .sort((a, b) => a[0].localeCompare(b[0]));
257 return sortedHeaders
258 .filter(([, value]) => value !== undefined)
259 .map(([headerName, value]) => {
260 // - Convert Array (multi-valued header) into string, delimited by
261 // ',' (no space).
262 // - Trim leading and trailing spaces.
263 // - Convert sequential (2+) spaces into a single space
264 const canonicalValue = `${value}`.trim().replace(/\s{2,}/g, ' ');
265 return `${headerName}:${canonicalValue}\n`;
266 })
267 .join('');
268 }
269 getCanonicalRequest(method, path, query, headers, signedHeaders, contentSha256) {
270 return [
271 method,
272 path,
273 query,
274 headers,
275 signedHeaders,
276 contentSha256 || 'UNSIGNED-PAYLOAD',
277 ].join('\n');
278 }
279 getCanonicalQueryParams(query) {
280 return (0, util_js_1.objectEntries)(query)
281 .map(([key, value]) => [(0, util_js_1.encodeURI)(key, true), (0, util_js_1.encodeURI)(value, true)])
282 .sort((a, b) => (a[0] < b[0] ? -1 : 1))
283 .map(([key, value]) => `${key}=${value}`)
284 .join('&');
285 }
286 getResourcePath(cname, bucket, file) {
287 if (cname) {
288 return '/' + (file || '');
289 }
290 else if (file) {
291 return `/${bucket}/${file}`;
292 }
293 else {
294 return `/${bucket}`;
295 }
296 }
297 parseExpires(expires, current = new Date()) {
298 const expiresInMSeconds = new Date(expires).valueOf();
299 if (isNaN(expiresInMSeconds)) {
300 throw new Error(storage_js_1.ExceptionMessages.EXPIRATION_DATE_INVALID);
301 }
302 if (expiresInMSeconds < current.valueOf()) {
303 throw new Error(storage_js_1.ExceptionMessages.EXPIRATION_DATE_PAST);
304 }
305 return Math.floor(expiresInMSeconds / 1000); // The API expects seconds.
306 }
307 parseAccessibleAt(accessibleAt) {
308 const accessibleAtInMSeconds = new Date(accessibleAt || new Date()).valueOf();
309 if (isNaN(accessibleAtInMSeconds)) {
310 throw new Error(SignerExceptionMessages.ACCESSIBLE_DATE_INVALID);
311 }
312 return Math.floor(accessibleAtInMSeconds / 1000); // The API expects seconds.
313 }
314}
315exports.URLSigner = URLSigner;
316/**
317 * Custom error type for errors related to getting signed errors and policies.
318 *
319 * @private
320 */
321class SigningError extends Error {
322 constructor() {
323 super(...arguments);
324 this.name = 'SigningError';
325 }
326}
327exports.SigningError = SigningError;