Use the generator controls to request a signed URL and open it. Clicking the file tiles performs their native navigation.
/generate/)This JavaScript snippet produces tokens compatible with Cloudflare's is_timed_hmac_valid_v0() WAF function. Place this as a Worker/Snippet or endpoint that responds to /generate{path}.
// ============================================
// WAF-COMPATIBLE HMAC URL SIGNING SNIPPET
// This generates tokens compatible with Cloudflare's
// is_timed_hmac_valid_v0() WAF function
// https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#hmac-validation
// ============================================
const CONFIG = {
SECRET_KEY: 'mysecrettoken',
EXPIRY_SECONDS: 120, // Match WAF ttl parameter (seconds)
SEPARATOR: '?verify=', // Must match WAF lengthOfSeparator (8 bytes)
USE_URL_SAFE_BASE64: false, // Set to true if using 's' flag in WAF
};
const encoder = new TextEncoder();
function arrayBufferToBase64(buffer, urlSafe = false) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
if (urlSafe) {
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
return encodeURIComponent(base64);
}
async function importKey(secretKey) {
return crypto.subtle.importKey('raw', encoder.encode(secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
}
async function generateWafCompatibleToken(url, key) {
url.pathname = url.pathname.replace('/generate/', '/');
const message = url.pathname;
const timestamp = Math.floor(Date.now() / 1000);
const dataToSign = `${message}${timestamp}`;
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(dataToSign));
const mac = arrayBufferToBase64(signature, CONFIG.USE_URL_SAFE_BASE64);
const token = `${timestamp}-${mac}`;
return `${url.pathname}${CONFIG.SEPARATOR}${token}`;
}
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname.startsWith('/generate/')) {
try {
const key = await importKey(CONFIG.SECRET_KEY);
const signedUrl = await generateWafCompatibleToken(url, key);
return new Response(signedUrl, { status: 200, headers: { 'Content-Type': 'text/plain', 'X-Token-Format': 'waf-compatible' } });
} catch (err) {
return new Response('Failed to generate signed URL', { status: 500 });
}
}
return fetch(request);
},
};
The WAF expression validates the signed URL on requests to /downloads/. It uses is_timed_hmac_valid_v0() with the same secret and parameters used by the generator. See Cloudflare's documentation for the function here: HMAC validation function.
(http.host eq "hmac.automatic-demo.com" and not is_timed_hmac_valid_v0("mysecrettoken", http.request.uri, 120, http.request.timestamp.sec, 8)) and (starts_with(http.request.uri.path, "/downloads/"))
It is recommended to create a Cache Rule with "Ignore Query String" to allow proper caching of the signed resources under /downloads/. Configure the rule to match the URI path /downloads/. Guidance: Cache levels & caching rules.
/downloads/.120 seconds).?verify=). Its byte length must match the WAF's lengthOfSeparator parameter (8 bytes in the example).encodeURIComponent(). The WAF must be configured accordingly (the 's' flag).is_timed_hmac_valid_v0(secret, message, ttl, now, lengthOfSeparator) – verifies the HMAC and timestamp. The message passed to the function is typically http.request.uri, and now is http.request.timestamp.sec./downloads/ and set it to Ignore Query String, so cached responses are served regardless of the appended verification token query. See Cloudflare guidance on caching levels and when to use cache rules: Cache levels & caching rules and when to use snippets.