labels
This commit is contained in:
90
server.js
90
server.js
@@ -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,12 +19,10 @@ 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.MAX_NOTIFICATION_RETRIES || 3, 10);
|
||||
const initialRetryDelay = parseInt(process.env.INITIAL_RETRY_DELAY_MS || 1000, 10); // 1 second
|
||||
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
|
||||
const firstRetryDelay = parseInt(process.env.NOTIFICATION_FIRST_RETRY_DELAY_MS || 10, 10); // 10 milliseconds - minimal delay for first retry
|
||||
// HTTP request timeout configuration
|
||||
const httpTimeout = parseInt(process.env.HTTP_TIMEOUT_MS || 10000, 10); // 10 seconds
|
||||
// Logging level configuration
|
||||
@@ -98,28 +95,40 @@ dns.setServers(dns.getServers()); // Reset DNS servers (can help in some Docker
|
||||
// Example: dns.setServers(['8.8.8.8', '1.1.1.1']);
|
||||
|
||||
// --- Utility function for retrying web push notifications with exponential backoff ---
|
||||
async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = initialRetryDelay) {
|
||||
async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = subsequentRetryDelay) {
|
||||
try {
|
||||
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) {
|
||||
console.log(`DNS resolution failed (${error.code}). Retrying notification in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`);
|
||||
|
||||
// Wait for the delay
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// Exponential backoff with jitter
|
||||
const nextDelay = delay * (1.5 + Math.random() * 0.5);
|
||||
|
||||
// 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 :
|
||||
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;
|
||||
}
|
||||
@@ -169,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
|
||||
@@ -235,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]}`);
|
||||
});
|
||||
@@ -286,12 +279,12 @@ 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]}`);
|
||||
});
|
||||
|
||||
logger.info(`Received GET webhook: Button=${buttonName}, Type=${click_type}, Battery=${batteryLevel}, Timestamp=${timestamp || 'N/A'}`);
|
||||
logger.info(`Received GET webhook: Button=${buttonName}, Type=${click_type}, Battery=${batteryLevel}%, Timestamp=${timestamp || 'N/A'}`);
|
||||
|
||||
// Basic validation
|
||||
if (!click_type) {
|
||||
@@ -349,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');
|
||||
// Log Basic Auth status instead of Flic Secret
|
||||
if (basicAuthUsername && basicAuthPassword) {
|
||||
logger.info('Authentication: Basic Auth Enabled');
|
||||
@@ -359,8 +350,9 @@ server.listen(port, () => {
|
||||
logger.info('Authentication: Basic Auth Disabled (username/password not set)');
|
||||
}
|
||||
logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`);
|
||||
logger.info(`Webhook Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`);
|
||||
logger.info(`Subscriptions File: ${subscriptionsFilePath}`);
|
||||
logger.info(`Push Notification Retry Config: ${maxRetries} retries, ${initialRetryDelay}ms initial delay`);
|
||||
logger.info(`Push Notification Retry Config: ${maxRetries} retries, first retry: ${firstRetryDelay}ms, subsequent retries: ${subsequentRetryDelay}ms base delay`);
|
||||
logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);
|
||||
logger.info(`HTTP Timeout: ${httpTimeout}ms`);
|
||||
logger.info(`Log Level: ${LOG_LEVEL.toUpperCase()}`);
|
||||
|
||||
Reference in New Issue
Block a user