diff --git a/package.json b/package.json index a10e604ad..ebb72326e 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "async-retry": "^1.3.3", "compressible": "^2.0.12", "configstore": "^5.0.0", - "date-and-time": "^2.0.0", "duplexify": "^4.0.0", "ent": "^2.2.0", "extend": "^3.0.2", diff --git a/src/file.ts b/src/file.ts index 31d968d86..83a892fc2 100644 --- a/src/file.ts +++ b/src/file.ts @@ -25,7 +25,6 @@ import {promisifyAll} from '@google-cloud/promisify'; import compressible = require('compressible'); import * as crypto from 'crypto'; -import * as dateFormat from 'date-and-time'; import * as extend from 'extend'; import * as fs from 'fs'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -65,7 +64,12 @@ import { } from './nodejs-common/util'; // eslint-disable-next-line @typescript-eslint/no-var-requires const duplexify: DuplexifyConstructor = require('duplexify'); -import {normalize, objectKeyToLowercase, unicodeJSONStringify} from './util'; +import { + normalize, + objectKeyToLowercase, + unicodeJSONStringify, + formatAsUTCISO, +} from './util'; import retry = require('async-retry'); export type GetExpirationDateResponse = [Date]; @@ -2760,8 +2764,8 @@ class File extends ServiceObject { let fields = Object.assign({}, options.fields); const now = new Date(); - const nowISO = dateFormat.format(now, 'YYYYMMDD[T]HHmmss[Z]', true); - const todayISO = dateFormat.format(now, 'YYYYMMDD', true); + const nowISO = formatAsUTCISO(now, true); + const todayISO = formatAsUTCISO(now); const sign = async () => { const {client_email} = await this.storage.authClient.getCredentials(); @@ -2786,11 +2790,7 @@ class File extends ServiceObject { delete fields.bucket; - const expiration = dateFormat.format( - expires, - 'YYYY-MM-DD[T]HH:mm:ss[Z]', - true - ); + const expiration = formatAsUTCISO(expires, true, '-', ':'); const policy = { conditions, diff --git a/src/signer.ts b/src/signer.ts index af27b7214..8af898730 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -13,11 +13,10 @@ // limitations under the License. import * as crypto from 'crypto'; -import * as dateFormat from 'date-and-time'; import * as http from 'http'; import * as url from 'url'; import {ExceptionMessages} from './storage'; -import {encodeURI, qsStringify, objectEntries} from './util'; +import {encodeURI, qsStringify, objectEntries, formatAsUTCISO} from './util'; interface GetCredentialsResponse { client_email?: string; @@ -268,15 +267,14 @@ export class URLSigner { const extensionHeadersString = this.getCanonicalHeaders(extensionHeaders); - const datestamp = dateFormat.format(config.accessibleAt, 'YYYYMMDD', true); + const datestamp = formatAsUTCISO(config.accessibleAt); 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( + const dateISO = formatAsUTCISO( config.accessibleAt ? config.accessibleAt : new Date(), - 'YYYYMMDD[T]HHmmss[Z]', true ); const queryParams: Query = { diff --git a/src/util.ts b/src/util.ts index cb88b34c9..89ba6a2d5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -131,3 +131,38 @@ export function convertObjKeysToSnakeCase(obj: object): object { return obj; } + +/** + * Formats the provided date object as a UTC ISO string. + * @param {Date} dateTimeToFormat date object to be formatted. + * @param {boolean} includeTime flag to include hours, minutes, seconds in output. + * @param {string} dateDelimiter delimiter between date components. + * @param {string} timeDelimiter delimiter between time components. + * @returns {string} UTC ISO format of provided date obect. + */ +export function formatAsUTCISO( + dateTimeToFormat: Date, + includeTime = false, + dateDelimiter = '', + timeDelimiter = '' +): string { + const year = dateTimeToFormat.getUTCFullYear(); + const month = dateTimeToFormat.getUTCMonth() + 1; + const day = dateTimeToFormat.getUTCDate(); + const hour = dateTimeToFormat.getUTCHours(); + const minute = dateTimeToFormat.getUTCMinutes(); + const second = dateTimeToFormat.getUTCSeconds(); + + let resultString = `${year.toString().padStart(4, '0')}${dateDelimiter}${month + .toString() + .padStart(2, '0')}${dateDelimiter}${day.toString().padStart(2, '0')}`; + if (includeTime) { + resultString = `${resultString}T${hour + .toString() + .padStart(2, '0')}${timeDelimiter}${minute + .toString() + .padStart(2, '0')}${timeDelimiter}${second.toString().padStart(2, '0')}Z`; + } + + return resultString; +} diff --git a/test/file.ts b/test/file.ts index 132c25de7..154c5b36b 100644 --- a/test/file.ts +++ b/test/file.ts @@ -25,7 +25,6 @@ import {PromisifyAllOptions} from '@google-cloud/promisify'; import {Readable, PassThrough, Stream, Duplex, Transform} from 'stream'; import * as assert from 'assert'; import * as crypto from 'crypto'; -import * as dateFormat from 'date-and-time'; import * as duplexify from 'duplexify'; import * as extend from 'extend'; import * as fs from 'fs'; @@ -57,6 +56,7 @@ import { FileExceptionMessages, } from '../src/file'; import {ExceptionMessages, IdempotencyStrategy} from '../src/storage'; +import {formatAsUTCISO} from '../src/util'; class HTTPError extends Error { code: number; @@ -3324,11 +3324,7 @@ describe('File', () => { {bucket: BUCKET.name}, ...fieldsToConditions(requiredFields), ], - expiration: dateFormat.format( - new Date(CONFIG.expires), - 'YYYY-MM-DD[T]HH:mm:ss[Z]', - true - ), + expiration: formatAsUTCISO(new Date(CONFIG.expires), true, '-', ':'), }; const policyString = JSON.stringify(policy); @@ -3548,7 +3544,7 @@ describe('File', () => { ); assert.strictEqual( policy.expiration, - dateFormat.format(expires, 'YYYY-MM-DD[T]HH:mm:ss[Z]', true) + formatAsUTCISO(expires, true, '-', ':') ); done(); } @@ -3569,11 +3565,7 @@ describe('File', () => { ); assert.strictEqual( policy.expiration, - dateFormat.format( - new Date(expires), - 'YYYY-MM-DD[T]HH:mm:ss[Z]', - true - ) + formatAsUTCISO(new Date(expires), true, '-', ':') ); done(); } @@ -3581,10 +3573,10 @@ describe('File', () => { }); it('should accept strings', done => { - const expires = dateFormat.format( + const expires = formatAsUTCISO( new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), - 'YYYY-MM-DD', - true + false, + '-' ); file.generateSignedPostPolicyV4( @@ -3598,11 +3590,7 @@ describe('File', () => { ); assert.strictEqual( policy.expiration, - dateFormat.format( - new Date(expires), - 'YYYY-MM-DD[T]HH:mm:ss[Z]', - true - ) + formatAsUTCISO(new Date(expires), true, '-', ':') ); done(); } diff --git a/test/signer.ts b/test/signer.ts index 2c05b841c..9025ae31b 100644 --- a/test/signer.ts +++ b/test/signer.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. import * as assert from 'assert'; -import * as dateFormat from 'date-and-time'; import * as crypto from 'crypto'; import * as sinon from 'sinon'; import {describe, it, beforeEach, afterEach} from 'mocha'; @@ -29,7 +28,7 @@ import { Query, SignerExceptionMessages, } from '../src/signer'; -import {encodeURI, qsStringify} from '../src/util'; +import {encodeURI, formatAsUTCISO, qsStringify} from '../src/util'; import {ExceptionMessages} from '../src/storage'; describe('signer', () => { @@ -187,11 +186,7 @@ describe('signer', () => { expires: expiresNumber, }); const blobToSign = authClientSign.getCall(0).args[0]; - assert( - blobToSign.includes( - dateFormat.format(accessibleAt, 'YYYYMMDD[T]HHmmss[Z]', true) - ) - ); + assert(blobToSign.includes(formatAsUTCISO(accessibleAt, true))); }); it('should throw if an expiration date from the before accessibleAt date is given', () => { @@ -211,11 +206,7 @@ describe('signer', () => { describe('checkInputTypes', () => { const query = { - 'X-Goog-Date': dateFormat.format( - new Date(accessibleAtNumber), - 'YYYYMMDD[T]HHmmss[Z]', - true - ), + 'X-Goog-Date': formatAsUTCISO(new Date(accessibleAtNumber), true), }; it('should accept Date objects', async () => { @@ -688,7 +679,7 @@ describe('signer', () => { const query = (await signer['getSignedUrlV4'](CONFIG)) as Query; const arg = getCanonicalQueryParams.getCall(0).args[0]; - const datestamp = dateFormat.format(NOW, 'YYYYMMDD', true); + const datestamp = formatAsUTCISO(NOW); const credentialScope = `${datestamp}/auto/storage/goog4_request`; const EXPECTED_CREDENTIAL = `${CLIENT_EMAIL}/${credentialScope}`; @@ -697,7 +688,7 @@ describe('signer', () => { }); it('should populate X-Goog-Date', async () => { - const dateISO = dateFormat.format(NOW, 'YYYYMMDD[T]HHmmss[Z]', true); + const dateISO = formatAsUTCISO(NOW, true); const query = (await signer['getSignedUrlV4'](CONFIG)) as Query; const arg = getCanonicalQueryParams.getCall(0).args[0]; @@ -787,9 +778,9 @@ describe('signer', () => { }); it('should compose blobToSign', async () => { - const datestamp = dateFormat.format(NOW, 'YYYYMMDD', true); + const datestamp = formatAsUTCISO(NOW); const credentialScope = `${datestamp}/auto/storage/goog4_request`; - const dateISO = dateFormat.format(NOW, 'YYYYMMDD[T]HHmmss[Z]', true); + const dateISO = formatAsUTCISO(NOW, true); const authClientSign = sinon .stub(authClient, 'sign')