let traefik handle CORS

This commit is contained in:
cpu
2025-03-31 13:17:52 +02:00
parent 81e11c155e
commit 2120e965fb
6 changed files with 45 additions and 129 deletions

View File

@@ -1,6 +1,5 @@
const express = require('express');
const webpush = require('web-push');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const dns = require('dns'); // Add DNS module
@@ -20,9 +19,6 @@ const basicAuthUsername = process.env.BASIC_AUTH_USERNAME;
const basicAuthPassword = process.env.BASIC_AUTH_PASSWORD;
// Note: We are NOT adding specific authentication for the /subscribe endpoint in this version.
// Consider adding API key or other auth if exposing this publicly.
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin => origin.trim()).filter(origin => origin);
const allowedMethods = (process.env.ALLOWED_METHODS || "POST,OPTIONS,GET").split(',').map(method => method.trim()).filter(method => method);
const allowedHeaders = (process.env.ALLOWED_HEADERS || "Content-Type,Authorization").split(',').map(header => header.trim()).filter(header => header);
// Retry configuration for DNS resolution issues
const maxRetries = parseInt(process.env.NOTIFICATION_MAX_RETRIES || 3, 10);
const subsequentRetryDelay = parseInt(process.env.NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS || 1000, 10); // 1 second base delay for subsequent retries
@@ -104,35 +100,35 @@ async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay
return await webpush.sendNotification(subscription, payload);
} catch (error) {
// Check if the error is a DNS resolution error that might be temporary
const isDnsError = error.code === 'EAI_AGAIN' ||
error.code === 'ENOTFOUND' ||
const isDnsError = error.code === 'EAI_AGAIN' ||
error.code === 'ENOTFOUND' ||
error.code === 'ETIMEDOUT';
if (isDnsError && retryCount < maxRetries) {
// For first retry (retryCount = 0), use minimal delay or no delay
const actualDelay = retryCount === 0 ? firstRetryDelay : delay;
if (retryCount === 0) {
logger.info(`DNS resolution failed (${error.code}). Retrying notification immediately or with minimal delay of ${firstRetryDelay}ms (attempt ${retryCount + 1}/${maxRetries})...`);
} else {
logger.info(`DNS resolution failed (${error.code}). Retrying notification in ${actualDelay}ms (attempt ${retryCount + 1}/${maxRetries})...`);
}
// Wait for the delay (minimal or none for first retry)
if (actualDelay > 0) {
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
// Calculate next delay with exponential backoff + jitter
// First retry uses subsequentRetryDelay, subsequent retries use exponential increase
const nextDelay = retryCount === 0 ?
subsequentRetryDelay :
const nextDelay = retryCount === 0 ?
subsequentRetryDelay :
delay * (1.5 + Math.random() * 0.5);
// Retry recursively with increased count and delay
return sendWebPushWithRetry(subscription, payload, retryCount + 1, nextDelay);
}
// If we've exhausted retries or it's not a DNS error, rethrow
throw error;
}
@@ -182,34 +178,18 @@ loadSubscriptions();
// --- Express App Setup ---
const app = express();
// --- CORS Middleware ---
const corsOptions = {
origin: (origin, callback) => {
if (!origin || allowedOrigins.length === 0 || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.warn(`CORS: Blocked origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
methods: allowedMethods,
allowedHeaders: allowedHeaders,
optionsSuccessStatus: 204 // For pre-flight requests
};
app.use(cors(corsOptions));
// Enable pre-flight requests for all relevant routes
app.options('/webhook', cors(corsOptions));
app.options('/subscribe', cors(corsOptions));
// --- Body Parsing Middleware ---
app.use(express.json());
// --- Basic Authentication Middleware ---
const authenticateBasic = (req, res, next) => {
// Skip authentication for OPTIONS requests (CORS preflight)
// Skip authentication for OPTIONS requests (CORS preflight - Traefik might still forward them or handle them)
// It's safe to keep this check.
if (req.method === 'OPTIONS') {
return next();
logger.debug('Auth: Skipping auth for OPTIONS request.');
// Traefik should ideally respond to OPTIONS, but if it forwards, we just proceed without auth check.
// We don't need to send CORS headers here anymore.
return res.sendStatus(204); // Standard practice for preflight response if it reaches the backend
}
// Skip authentication if username or password are not set in environment
@@ -248,8 +228,8 @@ const authenticateBasic = (req, res, next) => {
// Apply Basic Authentication
app.post('/subscribe', authenticateBasic, async (req, res) => {
let { button_id, subscription } = req.body;
logger.debug('All headers received:');
logger.debug('All headers received on /subscribe:');
Object.keys(req.headers).forEach(headerName => {
logger.debug(` ${headerName}: ${req.headers[headerName]}`);
});
@@ -299,7 +279,7 @@ app.get('/webhook/:click_type', authenticateBasic, async (req, res) => {
const batteryLevel = batteryLevelHeader ? parseInt(batteryLevelHeader, 10) || batteryLevelHeader : 'N/A';
// Log all headers received from Flic
logger.debug('All headers received:');
logger.debug('All headers received on /webhook:');
Object.keys(req.headers).forEach(headerName => {
logger.debug(` ${headerName}: ${req.headers[headerName]}`);
});
@@ -362,9 +342,7 @@ const server = http.createServer(app);
server.listen(port, () => {
logger.info(`Flic Webhook to WebPush server listening on port ${port}`);
logger.info(`Allowed Origins: ${allowedOrigins.length > 0 ? allowedOrigins.join(', ') : '(Any)'}`);
logger.info(`Allowed Methods: ${allowedMethods.join(', ')}`);
logger.info(`Allowed Headers: ${allowedHeaders.join(', ')}`);
logger.info('CORS: Handled by Traefik (expected)');
// Log Basic Auth status instead of Flic Secret
if (basicAuthUsername && basicAuthPassword) {
logger.info('Authentication: Basic Auth Enabled');