diff --git a/.github/workflows/cdn-signed-urls.yaml b/.github/workflows/cdn-signed-urls.yaml new file mode 100644 index 0000000000..ed59e80b0a --- /dev/null +++ b/.github/workflows/cdn-signed-urls.yaml @@ -0,0 +1,52 @@ +# Copyright 2023 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. + +name: cdn-signed-urls +on: + push: + branches: + - main + paths: + - 'cdn/signed-urls/**' + - '.github/workflows/cdn-signed-urls.yaml' + pull_request: + types: + - opened + - reopened + - synchronize + - labeled + paths: + - 'cdn/signed-urls/**' + - '.github/workflows/cdn-signed-urls.yaml' + schedule: + - cron: '0 0 * * 0' +jobs: + test: + # Ref: https://github.com/google-github-actions/auth#usage + permissions: + contents: 'read' + id-token: 'write' + if: github.event.action != 'labeled' || github.event.label.name == 'actions:force-run' + uses: ./.github/workflows/test.yaml + with: + name: 'cdn-signed-urls' + path: 'cdn/signed-urls' + flakybot: + # Ref: https://github.com/google-github-actions/auth#usage + permissions: + contents: 'read' + id-token: 'write' + if: github.event_name == 'schedule' && always() # always() submits logs even if tests fail + uses: ./.github/workflows/flakybot.yaml + needs: [test] diff --git a/.github/workflows/utils/workflows.json b/.github/workflows/utils/workflows.json index ec662d7be7..31bb7c0e4f 100644 --- a/.github/workflows/utils/workflows.json +++ b/.github/workflows/utils/workflows.json @@ -18,6 +18,7 @@ "asset/snippets", "auth", "batch", + "cdn/signed-urls", "cloudbuild", "cloud-language", "cloud-tasks/snippets", diff --git a/cdn/signed-urls/package.json b/cdn/signed-urls/package.json new file mode 100644 index 0000000000..e14a11b2c8 --- /dev/null +++ b/cdn/signed-urls/package.json @@ -0,0 +1,18 @@ +{ + "name": "signed-urls-samples", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "mocha -p -j 2 **/*.test.js" + }, + "devDependencies": { + "mocha": "^10.0.0" + } +} \ No newline at end of file diff --git a/cdn/signed-urls/signurl.js b/cdn/signed-urls/signurl.js new file mode 100644 index 0000000000..3b556609df --- /dev/null +++ b/cdn/signed-urls/signurl.js @@ -0,0 +1,48 @@ +// Copyright 2018 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. + +'use strict'; + +// [START nodejs_cdn_signed_urls] +const crypto = require('crypto'); + +/** + * Sign url to access Google Cloud CDN resource secured by key. + * @param url the Cloud CDN endpoint to sign + * @param keyName name of the signing key configured in the backend service/bucket + * @param keyValue value of the signing key + * @param expirationDate the date that the signed URL expires + * @return signed CDN URL + */ +function signUrl(url, keyName, keyValue, expirationDate) { + const urlObject = new URL(url); + urlObject.searchParams.set( + 'Expires', + Math.floor(expirationDate.valueOf() / 1000).toString() + ); + urlObject.searchParams.set('KeyName', keyName); + + const signature = crypto + .createHmac('sha1', Buffer.from(keyValue, 'base64')) + .update(urlObject.href) + .digest('base64url'); + urlObject.searchParams.set('Signature', signature); + + return urlObject.href; +} +// [END nodejs_cdn_signed_urls] + +module.exports = { + signUrl, +}; diff --git a/cdn/signed-urls/test/signurl.test.js b/cdn/signed-urls/test/signurl.test.js new file mode 100644 index 0000000000..4d77bf82cc --- /dev/null +++ b/cdn/signed-urls/test/signurl.test.js @@ -0,0 +1,39 @@ +// Copyright 2018 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. + +'use strict'; + +const {signUrl} = require('../signurl'); +const assert = require('node:assert/strict'); + +describe('signUrl', () => { + it('should return signed url with corresponding parameters', () => { + const url = new URL('https://cdn.example.com/test-path'); + const keyName = 'test-key-name'; + const keyValue = '0zY26LFZ2yAa3fERaKiKDQ=='; + const expirationDate = new Date(); + + const resultUrl = new URL( + signUrl(url.href, keyName, keyValue, expirationDate) + ); + assert.strictEqual(resultUrl.hostname, url.hostname); + assert.strictEqual(resultUrl.pathname, url.pathname); + assert.strictEqual(resultUrl.searchParams.get('KeyName'), keyName); + assert.strictEqual( + resultUrl.searchParams.get('Expires'), + Math.floor(expirationDate.valueOf() / 1000).toString() + ); + assert.ok(resultUrl.searchParams.get('Signature')); + }); +});