fix: improve CORS proxy handling and update documentation for certificate fetching
This commit is contained in:
@@ -1,39 +1,38 @@
|
||||
/**
|
||||
* BentoPDF CORS Proxy Worker
|
||||
*
|
||||
*
|
||||
* This Cloudflare Worker proxies certificate requests for the digital signing tool.
|
||||
* It fetches certificates from external CAs that don't have CORS headers enabled
|
||||
* and returns them with proper CORS headers.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Deploy: npx wrangler deploy
|
||||
*
|
||||
*
|
||||
* Required Environment Variables (set in wrangler.toml or Cloudflare dashboard):
|
||||
* - PROXY_SECRET: Shared secret for HMAC signature verification
|
||||
*/
|
||||
|
||||
const ALLOWED_PATTERNS = [
|
||||
/\.crt$/i,
|
||||
/\.cer$/i,
|
||||
/\.pem$/i,
|
||||
/\/certs\//i,
|
||||
/\/ocsp/i,
|
||||
/\/crl/i,
|
||||
/caIssuers/i,
|
||||
const ALLOWED_PATH_PATTERNS = [
|
||||
/\.crt$/i,
|
||||
/\.cer$/i,
|
||||
/\.pem$/i,
|
||||
/\/certs\//i,
|
||||
/\/ocsp/i,
|
||||
/\/crl/i,
|
||||
/caIssuers/i,
|
||||
];
|
||||
|
||||
const ALLOWED_ORIGINS = [
|
||||
'https://www.bentopdf.com',
|
||||
'https://bentopdf.com',
|
||||
];
|
||||
const ALLOWED_ORIGINS = ['https://www.bentopdf.com', 'https://bentopdf.com'];
|
||||
|
||||
const BLOCKED_DOMAINS = [
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'0.0.0.0',
|
||||
const SAFE_CONTENT_TYPES = [
|
||||
'application/x-x509-ca-cert',
|
||||
'application/pkix-cert',
|
||||
'application/x-pem-file',
|
||||
'application/pkcs7-mime',
|
||||
'application/octet-stream',
|
||||
'text/plain',
|
||||
];
|
||||
|
||||
|
||||
const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000;
|
||||
|
||||
const RATE_LIMIT_MAX_REQUESTS = 60;
|
||||
@@ -42,310 +41,415 @@ const RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
||||
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
async function verifySignature(message, signature, secret) {
|
||||
try {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
|
||||
const signatureBytes = new Uint8Array(
|
||||
signature.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
|
||||
);
|
||||
|
||||
return await crypto.subtle.verify(
|
||||
'HMAC',
|
||||
key,
|
||||
signatureBytes,
|
||||
encoder.encode(message)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Signature verification error:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSignature(message, secret) {
|
||||
try {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
key,
|
||||
encoder.encode(message)
|
||||
const signatureBytes = new Uint8Array(
|
||||
signature.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
||||
);
|
||||
|
||||
return Array.from(new Uint8Array(signature))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
return await crypto.subtle.verify(
|
||||
'HMAC',
|
||||
key,
|
||||
signatureBytes,
|
||||
encoder.encode(message)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Signature verification error:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isAllowedOrigin(origin) {
|
||||
if (!origin) return false;
|
||||
return ALLOWED_ORIGINS.some(allowed => origin.startsWith(allowed.replace(/\/$/, '')));
|
||||
if (!origin) return false;
|
||||
return ALLOWED_ORIGINS.includes(origin);
|
||||
}
|
||||
|
||||
function isPrivateOrReservedHost(hostname) {
|
||||
if (
|
||||
/^10\./.test(hostname) ||
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) ||
|
||||
/^192\.168\./.test(hostname) ||
|
||||
/^169\.254\./.test(hostname) || // link-local (cloud metadata)
|
||||
/^100\.(6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])\./.test(hostname) || // CGNAT
|
||||
/^127\./.test(hostname) ||
|
||||
/^0\./.test(hostname)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(hostname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lower = hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
||||
if (
|
||||
lower === '::1' ||
|
||||
lower.startsWith('::ffff:') ||
|
||||
lower.startsWith('fe80') ||
|
||||
lower.startsWith('fc') ||
|
||||
lower.startsWith('fd') ||
|
||||
lower.startsWith('ff')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const blockedNames = [
|
||||
'localhost',
|
||||
'localhost.localdomain',
|
||||
'0.0.0.0',
|
||||
'[::1]',
|
||||
];
|
||||
if (blockedNames.includes(hostname.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isValidCertificateUrl(urlString) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BLOCKED_DOMAINS.some(domain => url.hostname.includes(domain))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostname = url.hostname;
|
||||
if (/^10\./.test(hostname) ||
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) ||
|
||||
/^192\.168\./.test(hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ALLOWED_PATTERNS.some(pattern => pattern.test(urlString));
|
||||
} catch {
|
||||
return false;
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPrivateOrReservedHost(url.hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ALLOWED_PATH_PATTERNS.some((pattern) => pattern.test(url.pathname));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSafeContentType(upstreamContentType) {
|
||||
if (!upstreamContentType) return 'application/octet-stream';
|
||||
const match = SAFE_CONTENT_TYPES.find((ct) =>
|
||||
upstreamContentType.startsWith(ct)
|
||||
);
|
||||
return match || 'application/octet-stream';
|
||||
}
|
||||
|
||||
function corsHeaders(origin) {
|
||||
return {
|
||||
'Access-Control-Allow-Origin': origin || '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
return {
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
}
|
||||
|
||||
function handleOptions(request) {
|
||||
const origin = request.headers.get('Origin');
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
const origin = request.headers.get('Origin');
|
||||
if (!isAllowedOrigin(origin)) {
|
||||
return new Response(null, { status: 403 });
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const origin = request.headers.get('Origin');
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const origin = request.headers.get('Origin');
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
return handleOptions(request);
|
||||
if (request.method === 'OPTIONS') {
|
||||
return handleOptions(request);
|
||||
}
|
||||
|
||||
// NOTE: If you are selfhosting this proxy, you can remove this check, or can set it to only accept requests from your own domain
|
||||
if (!isAllowedOrigin(origin)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Forbidden',
|
||||
message: 'This proxy only accepts requests from allowed origins',
|
||||
}),
|
||||
{
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: If you are selfhosting this proxy, you can remove this check, or can set it to only accept requests from your own domain
|
||||
if (!isAllowedOrigin(origin)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Forbidden',
|
||||
message: 'This proxy only accepts requests from bentopdf.com',
|
||||
}), {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('Method not allowed', {
|
||||
status: 405,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
}
|
||||
|
||||
const targetUrl = url.searchParams.get('url');
|
||||
|
||||
if (!targetUrl) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Missing url parameter',
|
||||
usage: 'GET /?url=<certificate_url>',
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('Method not allowed', {
|
||||
status: 405,
|
||||
headers: corsHeaders(origin),
|
||||
});
|
||||
if (!isValidCertificateUrl(targetUrl)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Invalid or disallowed URL',
|
||||
message:
|
||||
'Only certificate-related URLs are allowed (*.crt, *.cer, *.pem, /certs/, /ocsp, /crl)',
|
||||
}),
|
||||
{
|
||||
status: 403,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = url.searchParams.get('url');
|
||||
const timestamp = url.searchParams.get('t');
|
||||
const signature = url.searchParams.get('sig');
|
||||
if (env.PROXY_SECRET) {
|
||||
const timestamp = url.searchParams.get('t');
|
||||
const signature = url.searchParams.get('sig');
|
||||
|
||||
if (env.PROXY_SECRET) {
|
||||
if (!timestamp || !signature) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Missing authentication parameters',
|
||||
message: 'Request must include timestamp (t) and signature (sig) parameters',
|
||||
}), {
|
||||
status: 401,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const requestTime = parseInt(timestamp, 10);
|
||||
const now = Date.now();
|
||||
if (isNaN(requestTime) || Math.abs(now - requestTime) > MAX_TIMESTAMP_AGE_MS) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Request expired or invalid timestamp',
|
||||
message: 'Timestamp must be within 5 minutes of current time',
|
||||
}), {
|
||||
status: 401,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const message = `${targetUrl}${timestamp}`;
|
||||
const isValid = await verifySignature(message, signature, env.PROXY_SECRET);
|
||||
|
||||
if (!isValid) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid signature',
|
||||
message: 'Request signature verification failed',
|
||||
}), {
|
||||
status: 401,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!timestamp || !signature) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Missing authentication parameters',
|
||||
message:
|
||||
'Request must include timestamp (t) and signature (sig) parameters',
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const requestTime = parseInt(timestamp, 10);
|
||||
const now = Date.now();
|
||||
if (
|
||||
isNaN(requestTime) ||
|
||||
Math.abs(now - requestTime) > MAX_TIMESTAMP_AGE_MS
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Request expired or invalid timestamp',
|
||||
message: 'Timestamp must be within 5 minutes of current time',
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const message = `${targetUrl}${timestamp}`;
|
||||
const isValid = await verifySignature(
|
||||
message,
|
||||
signature,
|
||||
env.PROXY_SECRET
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Invalid signature',
|
||||
message: 'Request signature verification failed',
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
|
||||
const rateLimitKey = `ratelimit:${clientIP}`;
|
||||
const now = Date.now();
|
||||
|
||||
if (env.RATE_LIMIT_KV) {
|
||||
const rateLimitData = await env.RATE_LIMIT_KV.get(rateLimitKey, {
|
||||
type: 'json',
|
||||
});
|
||||
const requests = rateLimitData?.requests || [];
|
||||
|
||||
const recentRequests = requests.filter(
|
||||
(t) => now - t < RATE_LIMIT_WINDOW_MS
|
||||
);
|
||||
|
||||
if (recentRequests.length >= RATE_LIMIT_MAX_REQUESTS) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Rate limit exceeded',
|
||||
message: `Maximum ${RATE_LIMIT_MAX_REQUESTS} requests per minute. Please try again later.`,
|
||||
retryAfter: Math.ceil(
|
||||
(recentRequests[0] + RATE_LIMIT_WINDOW_MS - now) / 1000
|
||||
),
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
'Retry-After': Math.ceil(
|
||||
(recentRequests[0] + RATE_LIMIT_WINDOW_MS - now) / 1000
|
||||
).toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
recentRequests.push(now);
|
||||
await env.RATE_LIMIT_KV.put(
|
||||
rateLimitKey,
|
||||
JSON.stringify({ requests: recentRequests }),
|
||||
{
|
||||
expirationTtl: 120,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'[CORS Proxy] RATE_LIMIT_KV not configured — rate limiting is disabled'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'BentoPDF-CertProxy/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Failed to fetch certificate',
|
||||
status: response.status,
|
||||
}),
|
||||
{
|
||||
status: response.status,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check Content-Length header first (fast reject for known-large responses)
|
||||
const contentLength = parseInt(
|
||||
response.headers.get('Content-Length') || '0',
|
||||
10
|
||||
);
|
||||
if (contentLength > MAX_FILE_SIZE_BYTES) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'File too large',
|
||||
message: `Certificate file exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024}KB`,
|
||||
}),
|
||||
{
|
||||
status: 413,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const chunks = [];
|
||||
let totalSize = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
totalSize += value.byteLength;
|
||||
if (totalSize > MAX_FILE_SIZE_BYTES) {
|
||||
reader.cancel();
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'File too large',
|
||||
message: `Certificate file exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024}KB`,
|
||||
}),
|
||||
{
|
||||
status: 413,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!targetUrl) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Missing url parameter',
|
||||
usage: 'GET /?url=<certificate_url>',
|
||||
}), {
|
||||
status: 400,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
const certData = new Uint8Array(totalSize);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
certData.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
return new Response(certData, {
|
||||
status: 200,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': getSafeContentType(
|
||||
response.headers.get('Content-Type')
|
||||
),
|
||||
'Content-Length': totalSize.toString(),
|
||||
'Cache-Control': 'public, max-age=86400',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Proxy fetch error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Proxy error',
|
||||
message: 'Failed to fetch the requested certificate',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
|
||||
if (!isValidCertificateUrl(targetUrl)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid or disallowed URL',
|
||||
message: 'Only certificate-related URLs are allowed (*.crt, *.cer, *.pem, /certs/, /ocsp, /crl)',
|
||||
}), {
|
||||
status: 403,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
|
||||
const rateLimitKey = `ratelimit:${clientIP}`;
|
||||
const now = Date.now();
|
||||
|
||||
if (env.RATE_LIMIT_KV) {
|
||||
const rateLimitData = await env.RATE_LIMIT_KV.get(rateLimitKey, { type: 'json' });
|
||||
const requests = rateLimitData?.requests || [];
|
||||
|
||||
const recentRequests = requests.filter(t => now - t < RATE_LIMIT_WINDOW_MS);
|
||||
|
||||
if (recentRequests.length >= RATE_LIMIT_MAX_REQUESTS) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Rate limit exceeded',
|
||||
message: `Maximum ${RATE_LIMIT_MAX_REQUESTS} requests per minute. Please try again later.`,
|
||||
retryAfter: Math.ceil((recentRequests[0] + RATE_LIMIT_WINDOW_MS - now) / 1000),
|
||||
}), {
|
||||
status: 429,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
'Retry-After': Math.ceil((recentRequests[0] + RATE_LIMIT_WINDOW_MS - now) / 1000).toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
recentRequests.push(now);
|
||||
await env.RATE_LIMIT_KV.put(rateLimitKey, JSON.stringify({ requests: recentRequests }), {
|
||||
expirationTtl: 120,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'BentoPDF-CertProxy/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to fetch certificate',
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}), {
|
||||
status: response.status,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10);
|
||||
if (contentLength > MAX_FILE_SIZE_BYTES) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'File too large',
|
||||
message: `Certificate file exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024}KB`,
|
||||
size: contentLength,
|
||||
maxSize: MAX_FILE_SIZE_BYTES,
|
||||
}), {
|
||||
status: 413,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const certData = await response.arrayBuffer();
|
||||
|
||||
if (certData.byteLength > MAX_FILE_SIZE_BYTES) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'File too large',
|
||||
message: `Certificate file exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024}KB`,
|
||||
size: certData.byteLength,
|
||||
maxSize: MAX_FILE_SIZE_BYTES,
|
||||
}), {
|
||||
status: 413,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(certData, {
|
||||
status: 200,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': response.headers.get('Content-Type') || 'application/x-x509-ca-cert',
|
||||
'Content-Length': certData.byteLength.toString(),
|
||||
'Cache-Control': 'public, max-age=86400',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Proxy error',
|
||||
message: error.message,
|
||||
}), {
|
||||
status: 500,
|
||||
headers: {
|
||||
...corsHeaders(origin),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user