import * as crypto from 'crypto-js';
import { formatISO } from 'date-fns';
import { ConfigStore } from "fastly:config-store";
addEventListener('fetch', event => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
let req = removeQuery(event.request);
if (req.method === 'GET' || req.method === 'HEAD') {
authorizeRequest(req);
let res = await fetch(req, {
backend: 'gcs_backend',
});
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') {
req.headers.set('host', getConfig().HOST);
return await fetch(req, {
backend: 'gcs_backend',
});
} else {
return new Response("This method is not allowed", { status: 405 });
}
}
function authorizeRequest(req) {
const cfg = getConfig();
const timeStampISO8601Format = formatISO(new Date(), { format: 'basic' });
const YYYYMMDD = timeStampISO8601Format.slice(0, 8);
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=${[
cfg.ACCESS_ID,
YYYYMMDD,
cfg.GCS_REGION,
cfg.GCS_SERVICE,
'goog4_request'
].join('/')},SignedHeaders=${cfg.SIGNED_HEADERS},Signature=${signature}`;
console.log(`authorizationValue = ${authorizationValue}`);
req.headers.set('host', cfg.HOST);
req.headers.set('authorization', authorizationValue);
req.headers.set('x-goog-content-sha256',cfg.EMPTY_HASH);
req.headers.set('x-goog-date', timeStampISO8601Format);
}
function generateCanonicalRequest(req, timeStampISO8601Format) {
const httpMethod = req.method;
const url = new URL(req.url);
const decoded = decodeURIComponent(url.pathname);
const encoded = encodeURIComponent(decoded);
const canonicalUri = encoded.replaceAll('%2F', '/');
const cfg = getConfig();
const canonicalQuery = '';
const canonicalHeaders = `host:${cfg.HOST}\nx-goog-content-sha256:${cfg.EMPTY_HASH}\nx-goog-date:${timeStampISO8601Format}\n`;
return `${httpMethod}\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaders}\n${cfg.SIGNED_HEADERS}\n${cfg.EMPTY_HASH}`;
}
function generateStringToSign(timeStampISO8601Format, YYYYMMDD, canonicalRequest) {
const cfg = getConfig();
const scope = `${YYYYMMDD}/${cfg.GCS_REGION}/${cfg.GCS_SERVICE}/goog4_request`;
const hashedCanonicalRequest = crypto.SHA256(canonicalRequest);
return `GOOG4-HMAC-SHA256\n${timeStampISO8601Format}\n${scope}\n${hashedCanonicalRequest}`;
}
function generateSignature(YYYYMMDD, stringToSign) {
const cfg = getConfig();
const round1 = hmacSha256('GOOG4' + cfg.SECRET, YYYYMMDD);
const round2 = hmacSha256(round1, cfg.GCS_REGION);
const round3 = hmacSha256(round2, cfg.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 });
}
function getConfig() {
const config = new ConfigStore('gcs_config');
return {
EMPTY_HASH: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
ACCESS_ID: config.get('access_key_id'),
SECRET: config.get('secret_access_key'),
GCS_REGION: config.get('region'),
GCS_SERVICE: 'storage',
GCS_BUCKET_NAME: config.get('bucket'),
HOST: `${config.get('bucket')}.storage.googleapis.com`,
SIGNED_HEADERS: 'host;x-goog-content-sha256;x-goog-date',
};
}