Google Cloud Storage origin (private)

Use AWS compat mode to make authenticated requests to your GCS bucket.

VCL

Use this solution in your VCL service (click RUN below to test this solution or clone it to make changes):

Compute

Use this solution in your Compute service:

  1. JavaScript
  2. Rust
package.json
JavaScript
{"dependencies":{"crypto-js":"^4.1.1","date-fns":"^2.28.0"}}
index.js
JavaScript
import * as crypto from 'crypto-js';
import { formatISO } from 'date-fns';
// Hash of empty string, used for authentication string generation
// caculated from crypto.SHA256('');
const EMPTY_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
// The GCS access id that is linked to a specific service or user account.
const ACCESS_ID = 'XXXXXXXX'
// A 40-character Base-64 encoded string that is linked to the access ID above.
const SECRET = 'YYYYYYYY'
const GCS_REGION = 'asia-northeast1';
const GCS_SERVICE = 'storage';
const GCS_BUCKET_NAME = 'compute-at-edge-demo';
const HOST = `${GCS_BUCKET_NAME}.storage.googleapis.com`;
const SIGNED_HEADERS = 'host;x-goog-content-sha256;x-goog-date';
addEventListener('fetch', event => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
// Ignore the query string from client
let req = removeQuery(event.request);
// Only generate authorize header for GET and HEAD request
// Pass PURGE to backend as it is
// Block other kinds of method
if (req.method === 'GET' || req.method === 'HEAD') {
authorizeRequest(req);
let res = await fetch(req, {
backend: 'gcs_backend',
});
// Remove some headers returned from GCS
res.headers.delete('x-guploader-uploadid');
res.headers.delete('x-goog-hash');
res.headers.delete('x-goog-storage-class');
res.headers.delete('server');
res.headers.delete('alt-svc');
return res;
} else if (req.method === 'PURGE') {
// When doing the purge, we need to make sure the cache key matches
// Cache key is cacualted from URL and HOST header of request sent to backend
// URL of backend request has no query string
req.headers.set('host', HOST);
return await fetch(req, {
backend: 'gcs_backend',
});
} else {
return new Response("This method is not allowed", { status: 405 });
}
}
// Call the tasks and build the request with the authorization header
function authorizeRequest(req) {
const timeStampISO8601Format = formatISO(new Date(), { format: 'basic' });
const YYYYMMDD = timeStampISO8601Format.slice(0, 8);
console.log(YYYYMMDD);
const canonicalRequest = generateCanonicalRequest(req, timeStampISO8601Format);
console.log(`canonicalRequest = ${canonicalRequest}`);
const stringToSign = generateStringToSign(timeStampISO8601Format, YYYYMMDD, canonicalRequest);
console.log(`stringToSign = ${stringToSign}`);
const signature = generateSignature(YYYYMMDD, stringToSign);
console.log(`signature = ${signature}`);
const authorizationValue = `GOOG4-HMAC-SHA256 Credential=${ACCESS_ID}/${YYYYMMDD}/${GCS_REGION}/${GCS_SERVICE}/goog4_request,SignedHeaders=${SIGNED_HEADERS},Signature=${signature}`;
console.log(`authorizationValue = ${authorizationValue}`);
req.headers.set('host', HOST);
req.headers.set('authorization', authorizationValue);
req.headers.set('x-goog-content-sha256',EMPTY_HASH);
req.headers.set('x-goog-date', timeStampISO8601Format);
}
// Task 1: Create a Canonical Request
// https://cloud.google.com/storage/docs/authentication/canonical-requests
function generateCanonicalRequest(req, timeStampISO8601Format) {
const httpMethod = req.method;
// Do url decode and re-encode in case some client are not do url encoding.
// https://cloud.google.com/storage/docs/authentication/canonical-requests#about-resource-path
const url = new URL(req.url);
const decoded = decodeURIComponent(url.pathname);
const encoded = encodeURIComponent(decoded);
const canonicalUri = encoded.replaceAll('%2F', '/');
const canonicalQuery = '';
const canonicalHeaders = `host:${HOST}\nx-goog-content-sha256:${EMPTY_HASH}\nx-goog-date:${timeStampISO8601Format}\n`;
return `${httpMethod}\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaders}\n${SIGNED_HEADERS}\n${EMPTY_HASH}`;
}
// Task 2: Create a String to Sign
// https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign
function generateStringToSign(timeStampISO8601Format, YYYYMMDD, canonicalRequest) {
const scope = `${YYYYMMDD}/${GCS_REGION}/${GCS_SERVICE}/goog4_request`;
const hashedCanonicalRequest = crypto.SHA256(canonicalRequest);
return `GOOG4-HMAC-SHA256\n${timeStampISO8601Format}\n${scope}\n${hashedCanonicalRequest}`;
}
// Task 3: Calculate Signature
// https://cloud.google.com/storage/docs/authentication/signatures#signing-process
function generateSignature(YYYYMMDD, stringToSign) {
const round1 = hmacSha256('GOOG4' + SECRET, YYYYMMDD);
const round2 = hmacSha256(round1, GCS_REGION);
const round3 = hmacSha256(round2, GCS_SERVICE);
const round4 = hmacSha256(round3, 'goog4_request');
return hmacSha256(round4, stringToSign);
}
function removeQuery(req) {
let url = new URL(req.url);
url.search = new URLSearchParams();
return new Request(url.toJSON(), req);
}
function hmacSha256(signingKey, stringToSign) {
return crypto.HmacSHA256(stringToSign, signingKey, { asBytes: true });
}
This page is part of a series in the Static content use case.

User contributed notes

BETA

Do you see an error in this page? Do you have an interesting use case, example or edge case people should know about? Share your knowledge and help people who are reading this page! (Comments are moderated; for support, please contact Fastly support)