text: "use strict";
  // Copyright 2020 Google LLC
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  Object.defineProperty(exports, "__esModule", { value: true });
  const crypto = require("crypto");
  const dateFormat = require("date-and-time");
  const url = require("url");
  const util_1 = require("./util");
  /*
   * Default signing version for getSignedUrl is 'v2'.
   */
  const DEFAULT_SIGNING_VERSION = 'v2';
  const SEVEN_DAYS = 604800;
  /**
   * @const {string}
   * @private
   */
  exports.PATH_STYLED_HOST = 'https://storage.googleapis.com';
  class URLSigner {
      constructor(authClient, bucket, file) {
          this.bucket = bucket;
          this.file = file;
          this.authClient = authClient;
      }
      getSignedUrl(cfg) {
          const expiresInSeconds = this.parseExpires(cfg.expires);
          const method = cfg.method;
          let customHost;
          // Default style is `path`.
          const isVirtualHostedStyle = cfg.virtualHostedStyle || false;
          if (cfg.cname) {
              customHost = cfg.cname;
          }
          else if (isVirtualHostedStyle) {
              customHost = `https://${this.bucket.name}.storage.googleapis.com`;
          }
          const config = Object.assign({}, cfg, {
              method,
              expiration: expiresInSeconds,
              bucket: this.bucket.name,
              file: this.file ? util_1.encodeURI(this.file.name, false) : undefined,
          });
          if (customHost) {
              config.cname = customHost;
          }
          const version = cfg.version || DEFAULT_SIGNING_VERSION;
          let promise;
          if (version === 'v2') {
              promise = this.getSignedUrlV2(config);
          }
          else if (version === 'v4') {
              promise = this.getSignedUrlV4(config);
          }
          else {
              throw new Error(`Invalid signed URL version: ${version}. Supported versions are 'v2' and 'v4'.`);
          }
          return promise.then(query => {
              query = Object.assign(query, cfg.queryParams);
              const signedUrl = new url.URL(config.cname || exports.PATH_STYLED_HOST);
              signedUrl.pathname = this.getResourcePath(!!config.cname, this.bucket.name, config.file);
              // tslint:disable-next-line:no-any
              signedUrl.search = util_1.qsStringify(query);
              return signedUrl.href;
          });
      }
      getSignedUrlV2(config) {
          const canonicalHeadersString = this.getCanonicalHeaders(config.extensionHeaders || {});
          const resourcePath = this.getResourcePath(false, config.bucket, config.file);
          const blobToSign = [
              config.method,
              config.contentMd5 || '',
              config.contentType || '',
              config.expiration,
              canonicalHeadersString + resourcePath,
          ].join('\n');
          const sign = async () => {
              const authClient = this.authClient;
              try {
                  const signature = await authClient.sign(blobToSign);
                  const credentials = await authClient.getCredentials();
                  return {
                      GoogleAccessId: credentials.client_email,
                      Expires: config.expiration,
                      Signature: signature,
                  };
              }
              catch (err) {
                  const signingErr = new SigningError(err.message);
                  signingErr.stack = err.stack;
                  throw signingErr;
              }
          };
          return sign();
      }
      getSignedUrlV4(config) {
          const now = new Date();
          const nowInSeconds = Math.floor(now.valueOf() / 1000);
          const expiresPeriodInSeconds = config.expiration - nowInSeconds;
          // v4 limit expiration to be 7 days maximum
          if (expiresPeriodInSeconds > SEVEN_DAYS) {
              throw new Error(`Max allowed expiration is seven days (${SEVEN_DAYS} seconds).`);
          }
          const extensionHeaders = Object.assign({}, config.extensionHeaders);
          const fqdn = new url.URL(config.cname || exports.PATH_STYLED_HOST);
          extensionHeaders.host = fqdn.host;
          if (config.contentMd5) {
              extensionHeaders['content-md5'] = config.contentMd5;
          }
          if (config.contentType) {
              extensionHeaders['content-type'] = config.contentType;
          }
          let contentSha256;
          const sha256Header = extensionHeaders['x-goog-content-sha256'];
          if (sha256Header) {
              if (typeof sha256Header !== 'string' ||
                  !/[A-Fa-f0-9]{40}/.test(sha256Header)) {
                  throw new Error('The header X-Goog-Content-SHA256 must be a hexadecimal string.');
              }
              contentSha256 = sha256Header;
          }
          const signedHeaders = Object.keys(extensionHeaders)
              .map(header => header.toLowerCase())
              .sort()
              .join(';');
          const extensionHeadersString = this.getCanonicalHeaders(extensionHeaders);
          const datestamp = dateFormat.format(now, 'YYYYMMDD', true);
          const credentialScope = `${datestamp}/auto/storage/goog4_request`;
          const sign = async () => {
              const credentials = await this.authClient.getCredentials();
              const credential = `${credentials.client_email}/${credentialScope}`;
              const dateISO = dateFormat.format(now, 'YYYYMMDD[T]HHmmss[Z]', true);
              const queryParams = Object.assign({ 'X-Goog-Algorithm': 'GOOG4-RSA-SHA256', 'X-Goog-Credential': credential, 'X-Goog-Date': dateISO, 'X-Goog-Expires': expiresPeriodInSeconds.toString(10), 'X-Goog-SignedHeaders': signedHeaders }, (config.queryParams || {}));
              // tslint:disable-next-line:no-any
              const canonicalQueryParams = this.getCanonicalQueryParams(queryParams);
              const canonicalRequest = this.getCanonicalRequest(config.method, this.getResourcePath(!!config.cname, config.bucket, config.file), canonicalQueryParams, extensionHeadersString, signedHeaders, contentSha256);
              const hash = crypto
                  .createHash('sha256')
                  .update(canonicalRequest)
                  .digest('hex');
              const blobToSign = [
                  'GOOG4-RSA-SHA256',
                  dateISO,
                  credentialScope,
                  hash,
              ].join('\n');
              try {
                  const signature = await this.authClient.sign(blobToSign);
                  const signatureHex = Buffer.from(signature, 'base64').toString('hex');
                  const signedQuery = Object.assign({}, queryParams, {
                      'X-Goog-Signature': signatureHex,
                  });
                  return signedQuery;
              }
              catch (err) {
                  const signingErr = new SigningError(err.message);
                  signingErr.stack = err.stack;
                  throw signingErr;
              }
          };
          return sign();
      }
      /**
       * Create canonical headers for signing v4 url.
       *
       * The canonical headers for v4-signing a request demands header names are
       * first lowercased, followed by sorting the header names.
       * Then, construct the canonical headers part of the request:
       *  <lowercasedHeaderName> + ":" + Trim(<value>) + "\n"
       *  ..
       *  <lowercasedHeaderName> + ":" + Trim(<value>) + "\n"
       *
       * @param headers
       * @private
       */
      getCanonicalHeaders(headers) {
          // Sort headers by their lowercased names
          const sortedHeaders = util_1.objectEntries(headers)
              // Convert header names to lowercase
              .map(([headerName, value]) => [
              headerName.toLowerCase(),
              value,
          ])
              .sort((a, b) => a[0].localeCompare(b[0]));
          return sortedHeaders
              .filter(([_, value]) => value !== undefined)
              .map(([headerName, value]) => {
              // - Convert Array (multi-valued header) into string, delimited by
              //      ',' (no space).
              // - Trim leading and trailing spaces.
              // - Convert sequential (2+) spaces into a single space
              const canonicalValue = `${value}`.trim().replace(/\s{2,}/g, ' ');
              return `${headerName}:${canonicalValue}\n`;
          })
              .join('');
      }
      getCanonicalRequest(method, path, query, headers, signedHeaders, contentSha256) {
          return [
              method,
              path,
              query,
              headers,
              signedHeaders,
              contentSha256 || 'UNSIGNED-PAYLOAD',
          ].join('\n');
      }
      getCanonicalQueryParams(query) {
          return util_1.objectEntries(query)
              .map(([key, value]) => [util_1.encodeURI(key, true), util_1.encodeURI(value, true)])
              .sort((a, b) => (a[0] < b[0] ? -1 : 1))
              .map(([key, value]) => `${key}=${value}`)
              .join('&');
      }
      getResourcePath(cname, bucket, file) {
          if (cname) {
              return '/' + (file || '');
          }
          else if (file) {
              return `/${bucket}/${file}`;
          }
          else {
              return `/${bucket}`;
          }
      }
      parseExpires(expires, current = new Date()) {
          const expiresInMSeconds = new Date(expires).valueOf();
          if (isNaN(expiresInMSeconds)) {
              throw new Error('The expiration date provided was invalid.');
          }
          if (expiresInMSeconds < current.valueOf()) {
              throw new Error('An expiration date cannot be in the past.');
          }
          return Math.round(expiresInMSeconds / 1000); // The API expects seconds.
      }
  }
  exports.URLSigner = URLSigner;
  /**
   * Custom error type for errors related to getting signed errors and policies.
   *
   * @private
   */
  class SigningError extends Error {
      constructor() {
          super(...arguments);
          this.name = 'SigningError';
      }
  }
  exports.SigningError = SigningError;
  //# sourceMappingURL=signer.js.map