feat(cors-proxy): add anti-spoofing security measures
Security improvements for the Cloudflare Worker CORS proxy: - Add rate limiting per IP (60 requests/minute) using Cloudflare KV - Add file size limit (10MB max) to prevent abuse - Add HMAC signature verification (optional, for deterrence) - Add timestamp validation to prevent replay attacks - Block private IP ranges (localhost, 10.x, 192.168.x, 172.16-31.x) Client-side changes: - Add signature generation in digital-sign-pdf.ts - Add security warning about client-side secrets Documentation: - Update README with production security features - Update docs/self-hosting/cloudflare.md with CORS proxy section - Document KV setup for rate limiting - Add clear warnings about client-side HMAC limitations Files changed: - cloudflare/cors-proxy-worker.js - cloudflare/wrangler.toml - src/js/logic/digital-sign-pdf.ts - README.md - docs/self-hosting/cloudflare.md
This commit is contained in:
53
README.md
53
README.md
@@ -469,10 +469,55 @@ The **Digital Signature** tool uses a signing library that may need to fetch cer
|
|||||||
VITE_CORS_PROXY_URL=https://your-worker-url.workers.dev npm run build
|
VITE_CORS_PROXY_URL=https://your-worker-url.workers.dev npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
**Security Notes:**
|
#### Production Security Features
|
||||||
- The proxy only allows requests to certificate-related URLs (.crt, .cer, .pem, /certs/, /ocsp)
|
|
||||||
- It blocks requests to localhost and private IP ranges
|
The CORS proxy includes several security measures:
|
||||||
- You can customize `ALLOWED_ORIGINS` in `wrangler.toml` to restrict which domains can use your proxy
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **URL Restrictions** | Only allows certificate URLs (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`) |
|
||||||
|
| **Private IP Blocking** | Blocks requests to localhost, 10.x, 192.168.x, 172.16-31.x |
|
||||||
|
| **File Size Limit** | Rejects files larger than 10MB |
|
||||||
|
| **Rate Limiting** | 60 requests per IP per minute (requires KV) |
|
||||||
|
| **HMAC Signatures** | Optional client-side signing (limited protection) |
|
||||||
|
|
||||||
|
#### Enabling Rate Limiting (Recommended)
|
||||||
|
|
||||||
|
Rate limiting requires Cloudflare KV storage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cloudflare
|
||||||
|
|
||||||
|
# Create KV namespace
|
||||||
|
npx wrangler kv:namespace create "RATE_LIMIT_KV"
|
||||||
|
|
||||||
|
# Copy the returned ID and add to wrangler.toml:
|
||||||
|
# [[kv_namespaces]]
|
||||||
|
# binding = "RATE_LIMIT_KV"
|
||||||
|
# id = "YOUR_ID_HERE"
|
||||||
|
|
||||||
|
# Redeploy
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Free tier limits:** 100,000 reads/day, 1,000 writes/day (~300-500 signatures/day)
|
||||||
|
|
||||||
|
#### HMAC Signature Verification (Optional)
|
||||||
|
|
||||||
|
> **⚠️ Security Warning:** Client-side secrets can be extracted from bundled JavaScript. For production deployments with sensitive requirements, use your own backend server to proxy requests instead of embedding secrets in frontend code.
|
||||||
|
|
||||||
|
BentoPDF uses client-side HMAC as a deterrent against casual abuse, but accepts this tradeoff due to its fully client-side architecture. To enable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a secret
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Set on Cloudflare Worker
|
||||||
|
npx wrangler secret put PROXY_SECRET
|
||||||
|
|
||||||
|
# Set in build environment
|
||||||
|
VITE_CORS_PROXY_SECRET=your-secret npm run build
|
||||||
|
```
|
||||||
|
|
||||||
### 📦 Version Management
|
### 📦 Version Management
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
* It fetches certificates from external CAs that don't have CORS headers enabled
|
* It fetches certificates from external CAs that don't have CORS headers enabled
|
||||||
* and returns them with proper CORS headers.
|
* and returns them with proper CORS headers.
|
||||||
*
|
*
|
||||||
* Security: Only allows fetching certificate-related URLs
|
|
||||||
*
|
*
|
||||||
* Deploy: npx wrangler deploy
|
* 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 = [
|
const ALLOWED_PATTERNS = [
|
||||||
@@ -31,6 +33,62 @@ const BLOCKED_DOMAINS = [
|
|||||||
'0.0.0.0',
|
'0.0.0.0',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
const RATE_LIMIT_MAX_REQUESTS = 60;
|
||||||
|
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) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(secret),
|
||||||
|
{ name: 'HMAC', hash: 'SHA-256' },
|
||||||
|
false,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
'HMAC',
|
||||||
|
key,
|
||||||
|
encoder.encode(message)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.from(new Uint8Array(signature))
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
function isAllowedOrigin(origin) {
|
function isAllowedOrigin(origin) {
|
||||||
if (!origin) return false;
|
if (!origin) return false;
|
||||||
return ALLOWED_ORIGINS.some(allowed => origin.startsWith(allowed.replace(/\/$/, '')));
|
return ALLOWED_ORIGINS.some(allowed => origin.startsWith(allowed.replace(/\/$/, '')));
|
||||||
@@ -108,6 +166,54 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetUrl = url.searchParams.get('url');
|
const targetUrl = url.searchParams.get('url');
|
||||||
|
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 (!targetUrl) {
|
if (!targetUrl) {
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
@@ -135,6 +241,37 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
const response = await fetch(targetUrl, {
|
const response = await fetch(targetUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -156,8 +293,39 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
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, {
|
return new Response(certData, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -6,11 +6,44 @@ compatibility_date = "2024-01-01"
|
|||||||
# If you are self hosting change the name to your worker name
|
# If you are self hosting change the name to your worker name
|
||||||
# Run: npx wrangler deploy
|
# Run: npx wrangler deploy
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SECURITY FEATURES
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 1. SIGNATURE VERIFICATION (Optional - for anti-spoofing)
|
||||||
|
# - Generate secret: openssl rand -hex 32
|
||||||
|
# - Set secret: npx wrangler secret put PROXY_SECRET
|
||||||
|
# - Note: Secret is visible in frontend JS, so provides limited protection
|
||||||
|
#
|
||||||
|
# 2. RATE LIMITING (Recommended - requires KV)
|
||||||
|
# - Create KV namespace: npx wrangler kv:namespace create "RATE_LIMIT_KV"
|
||||||
|
# - Uncomment the kv_namespaces section below with the returned ID
|
||||||
|
# - Limits: 60 requests per IP per minute
|
||||||
|
#
|
||||||
|
# 3. FILE SIZE LIMIT
|
||||||
|
# - Automatic: Rejects files larger than 1MB
|
||||||
|
# - Certificates are typically <10KB, so this prevents abuse
|
||||||
|
#
|
||||||
|
# 4. URL RESTRICTIONS
|
||||||
|
# - Only certificate URLs allowed (*.crt, *.cer, *.pem, /certs/, etc.)
|
||||||
|
# - Blocks private IPs (localhost, 10.x, 192.168.x, 172.16-31.x)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# KV NAMESPACE FOR RATE LIMITING
|
||||||
|
# =============================================================================
|
||||||
|
# To enable rate limiting:
|
||||||
|
# 1. Run: npx wrangler kv:namespace create "RATE_LIMIT_KV"
|
||||||
|
# 2. Copy the returned id and uncomment the section below
|
||||||
|
#
|
||||||
|
# [[kv_namespaces]]
|
||||||
|
# binding = "RATE_LIMIT_KV"
|
||||||
|
# id = "YOUR_KV_NAMESPACE_ID_HERE"
|
||||||
|
|
||||||
# Optional: Custom domain routing
|
# Optional: Custom domain routing
|
||||||
# routes = [
|
# routes = [
|
||||||
# { pattern = "cors-proxy.bentopdf.com/*", zone_name = "bentopdf.com" }
|
# { pattern = "cors-proxy.bentopdf.com/*", zone_name = "bentopdf.com" }
|
||||||
# ]
|
# ]
|
||||||
|
|
||||||
# Optional: Environment variables
|
# Optional: Environment variables (for non-secret config)
|
||||||
# [vars]
|
# [vars]
|
||||||
# ALLOWED_ORIGINS = "https://www.bentopdf.com,https://bentopdf.com"
|
# ALLOWED_ORIGINS = "https://www.bentopdf.com,https://bentopdf.com"
|
||||||
|
|||||||
@@ -76,3 +76,44 @@ npm run build
|
|||||||
### Worker Size Limits
|
### Worker Size Limits
|
||||||
|
|
||||||
If using Cloudflare Workers for advanced routing, note the 1 MB limit for free plans.
|
If using Cloudflare Workers for advanced routing, note the 1 MB limit for free plans.
|
||||||
|
|
||||||
|
## CORS Proxy Worker (For Digital Signatures)
|
||||||
|
|
||||||
|
The Digital Signature tool requires a CORS proxy to fetch certificate chains. Deploy the included worker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cloudflare
|
||||||
|
npx wrangler login
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **URL Restrictions** | Only certificate URLs allowed |
|
||||||
|
| **File Size Limit** | Max 10MB per request |
|
||||||
|
| **Rate Limiting** | 60 req/IP/min (requires KV) |
|
||||||
|
| **Private IP Blocking** | Blocks localhost, internal IPs |
|
||||||
|
|
||||||
|
### Enable Rate Limiting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create KV namespace
|
||||||
|
npx wrangler kv:namespace create "RATE_LIMIT_KV"
|
||||||
|
|
||||||
|
# Add to wrangler.toml with returned ID:
|
||||||
|
# [[kv_namespaces]]
|
||||||
|
# binding = "RATE_LIMIT_KV"
|
||||||
|
# id = "YOUR_ID"
|
||||||
|
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build with Proxy URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** See [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required) for HMAC signature setup.
|
||||||
|
|||||||
@@ -124,11 +124,58 @@ export function parseCombinedPem(pemContent: string, password?: string): Certifi
|
|||||||
*/
|
*/
|
||||||
const CORS_PROXY_URL = import.meta.env.VITE_CORS_PROXY_URL || '';
|
const CORS_PROXY_URL = import.meta.env.VITE_CORS_PROXY_URL || '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared secret for signing proxy requests (HMAC-SHA256).
|
||||||
|
*
|
||||||
|
* SECURITY NOTE FOR PRODUCTION:
|
||||||
|
* Client-side secrets are NEVER truly hidden and they can be extracted from
|
||||||
|
* bundled JavaScript.
|
||||||
|
*
|
||||||
|
* For production deployments with sensitive requirements, you should:
|
||||||
|
* 1. Use your own backend server to proxy certificate requests
|
||||||
|
* 2. Keep the HMAC secret on your server ONLY (never in frontend code)
|
||||||
|
* 3. Have your frontend call your server, which then calls the CORS proxy
|
||||||
|
*
|
||||||
|
* This client-side HMAC provides limited protection (deters casual abuse)
|
||||||
|
* but should NOT be considered secure against determined attackers. BentoPDF
|
||||||
|
* accepts this tradeoff because of it's client side architecture.
|
||||||
|
*
|
||||||
|
* To enable (optional):
|
||||||
|
* 1. Generate a secret: openssl rand -hex 32
|
||||||
|
* 2. Set PROXY_SECRET on your Cloudflare Worker: npx wrangler secret put PROXY_SECRET
|
||||||
|
* 3. Set VITE_CORS_PROXY_SECRET in your build environment (must match PROXY_SECRET)
|
||||||
|
*/
|
||||||
|
const CORS_PROXY_SECRET = import.meta.env.VITE_CORS_PROXY_SECRET || '';
|
||||||
|
|
||||||
|
async function generateProxySignature(url: string, timestamp: number): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(CORS_PROXY_SECRET),
|
||||||
|
{ name: 'HMAC', hash: 'SHA-256' },
|
||||||
|
false,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = `${url}${timestamp}`;
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
'HMAC',
|
||||||
|
key,
|
||||||
|
encoder.encode(message)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.from(new Uint8Array(signature))
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom fetch wrapper that routes external certificate requests through a CORS proxy.
|
* Custom fetch wrapper that routes external certificate requests through a CORS proxy.
|
||||||
* The zgapdfsigner library tries to fetch issuer certificates from URLs embedded in the
|
* The zgapdfsigner library tries to fetch issuer certificates from URLs embedded in the
|
||||||
* certificate's AIA extension. When those servers don't have CORS enabled (like www.cert.fnmt.es),
|
* certificate's AIA extension. When those servers don't have CORS enabled (like www.cert.fnmt.es),
|
||||||
* the fetch fails. This wrapper routes such requests through our CORS proxy.
|
* the fetch fails. This wrapper routes such requests through our CORS proxy.
|
||||||
|
*
|
||||||
|
* If VITE_CORS_PROXY_SECRET is configured, requests include HMAC signatures for anti-spoofing.
|
||||||
*/
|
*/
|
||||||
function createCorsAwareFetch(): {
|
function createCorsAwareFetch(): {
|
||||||
wrappedFetch: typeof fetch;
|
wrappedFetch: typeof fetch;
|
||||||
@@ -150,8 +197,17 @@ function createCorsAwareFetch(): {
|
|||||||
) && !url.startsWith(window.location.origin);
|
) && !url.startsWith(window.location.origin);
|
||||||
|
|
||||||
if (isExternalCertificateUrl && CORS_PROXY_URL) {
|
if (isExternalCertificateUrl && CORS_PROXY_URL) {
|
||||||
const proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
|
let proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
|
||||||
|
|
||||||
|
if (CORS_PROXY_SECRET) {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const signature = await generateProxySignature(url, timestamp);
|
||||||
|
proxyUrl += `&t=${timestamp}&sig=${signature}`;
|
||||||
|
console.log(`[CORS Proxy] Routing signed certificate request through proxy: ${url}`);
|
||||||
|
} else {
|
||||||
console.log(`[CORS Proxy] Routing certificate request through proxy: ${url}`);
|
console.log(`[CORS Proxy] Routing certificate request through proxy: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
return originalFetch(proxyUrl, init);
|
return originalFetch(proxyUrl, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user