diff --git a/.dockerignore b/.dockerignore index 9319e10..06749c9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,11 @@ Dockerfile .gitignore README.md *.example +images +.env +env +labels +subscriptions.json + + diff --git a/.env.example b/.env.example index 7410146..7fb422f 100644 --- a/.env.example +++ b/.env.example @@ -4,12 +4,13 @@ # Generate using: npx web-push generate-vapid-keys VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= -VAPID_SUBJECT=mailto:mailto:admin@virtonline.eu # Contact email/URL for push service +VAPID_SUBJECT=mailto:mailto:user@example.org # --- Server Configuration --- -PORT=3000 # Internal port for the Node.js app -SUBSCRIPTIONS_FILE=subscriptions.json # Path inside the container -DEFAULT_BUTTON_NAME=game-button # Default button name to use when not specified +# Internal port for the Node.js app +PORT=3000 +SUBSCRIPTIONS_FILE=/app/subscriptions.json +DEFAULT_BUTTON_NAME=game-button # --- Authentication (Optional) --- # If both USERNAME and PASSWORD are set, Basic Auth will be enabled for: @@ -19,21 +20,19 @@ DEFAULT_BUTTON_NAME=game-button # Default button name to use when not specified BASIC_AUTH_USERNAME=user12345 BASIC_AUTH_PASSWORD=password -# --- CORS Configuration (Optional but Recommended) --- -# Comma-separated list of allowed origins for requests (e.g., your PWA frontend URL) -# If blank or not set, CORS might block browser requests (like from a setup page). -# Use '*' carefully, preferably list specific domains. -ALLOWED_ORIGINS=https://game-timer.virtonline.eu -ALLOWED_METHODS=POST,GET,OPTIONS -ALLOWED_HEADERS=Content-Type,Authorization - # --- Web Push Retry Configuration (Optional) --- -MAX_NOTIFICATION_RETRIES=3 # Number of retries on failure (e.g., DNS issues) -INITIAL_RETRY_DELAY_MS=1000 # Initial delay in milliseconds +# Number of retries on failure (e.g., DNS issues) +NOTIFICATION_MAX_RETRIES=3 +# First retry delay in milliseconds (minimal delay for immediate retry) +NOTIFICATION_FIRST_RETRY_DELAY_MS=10 +# Base delay in milliseconds for subsequent retries (used for exponential backoff) +NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS=1000 # --- Network Configuration (Optional) --- -DNS_TIMEOUT_MS=5000 # Timeout for DNS lookups (ms) -HTTP_TIMEOUT_MS=10000 # Timeout for outgoing HTTP requests (ms) +# Timeout for DNS lookups (ms) +DNS_TIMEOUT_MS=5000 +# Timeout for outgoing HTTP requests (ms) +HTTP_TIMEOUT_MS=10000 # --- Logging --- # Controls log verbosity: error, warn, info, debug diff --git a/.gitignore b/.gitignore index d4f67a2..7df275b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ myenv .vscode .cursor +subscriptions.json # Node.js node_modules/ @@ -8,8 +9,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* .env -subscriptions.json - +env # OS generated files .DS_Store .DS_Store? diff --git a/README.md b/README.md index de7a074..0e60fcb 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for * Integrates with Traefik v3 via Docker labels. * Configurable via environment variables (`.env` file). * Optional Basic Authentication for securing the `/webhook` and `/subscribe` endpoints. -* CORS configuration for allowing requests (needed if your PWA management interface interacts with the `/subscribe` endpoint). ## Prerequisites @@ -61,20 +60,16 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for * `VAPID_PRIVATE_KEY`: The private key generated in step 2. **Keep this secret!** * `VAPID_SUBJECT`: A `mailto:` or `https:` URL identifying you or your application (e.g., `mailto:admin@yourdomain.com`). Used by push services to contact you. * `PORT`: (Default: `3000`) The internal port the Node.js app listens on. Traefik will map to this. - * `SUBSCRIPTIONS_FILE`: (Default: `/app/subscriptions.json`) The path *inside the container* where the button-to-subscription mapping is stored. + * `SUBSCRIPTIONS_FILE`: (Default: `subscriptions.json`) The path *inside the container* where the button-to-subscription mapping is stored. * `DEFAULT_BUTTON_NAME`: (Default: `game-button`) The default button name to use when the `Button-Name` header is not provided in the webhook request. * `BASIC_AUTH_USERNAME`: (Optional) Username for Basic Authentication. If set along with `BASIC_AUTH_PASSWORD`, authentication will be enabled for `/webhook` and `/subscribe`. * `BASIC_AUTH_PASSWORD`: (Optional) Password for Basic Authentication. If set along with `BASIC_AUTH_USERNAME`, authentication will be enabled. - * `ALLOWED_ORIGINS`: Comma-separated list of domains allowed by CORS. Include your PWA's domain if it needs to interact directly (e.g., for setup). Example: `https://my-pwa.com`. - * `ALLOWED_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed. - * `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed. - * `MAX_NOTIFICATION_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number. - * `INITIAL_RETRY_DELAY_MS`: (Default: `1000`) Initial delay in milliseconds before first retry. Must be a number. + * `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number. + * `NOTIFICATION_FIRST_RETRY_DELAY_MS`: (Default: `10`) Delay in milliseconds for the first retry attempt. Setting to 0-10ms provides near-immediate first retry for transient DNS issues. Must be a number. + * `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`: (Default: `1000`) Base delay in milliseconds for subsequent retries. Each additional retry uses this value with exponential backoff and jitter. Must be a number. * `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number. * `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. Must be a number. * `LOG_LEVEL`: (Default: `info`) Controls verbosity of logs. Valid values are `error`, `warn`, `info`, or `debug`. Use `debug` to see detailed header information and other diagnostic messages. - * `TRAEFIK_SERVICE_HOST`: Your public domain for this service (e.g., `webpush.virtonline.eu`). - * `TRAEFIK_CERT_RESOLVER`: The name of your TLS certificate resolver configured in Traefik (e.g., `le`, `myresolver`). 4. **Configure Traefik Labels:** * Copy the example `labels` file: @@ -82,24 +77,6 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for cp labels.example labels ``` -5. **Prepare Subscription Mapping File:** - * Edit the `subscriptions.json` file - * Add entries mapping your Flic button's serial number (as a lowercase string key) to the PWA `PushSubscription` object. - ```json - { - "game": { // <-- Replace with your actual Flic Button name (lowercase recommended) - "endpoint": "https://your_pwa_push_endpoint...", - "expirationTime": null, - "keys": { - "p256dh": "YOUR_PWA_SUBSCRIPTION_P256DH_KEY", - "auth": "YOUR_PWA_SUBSCRIPTION_AUTH_KEY" - } - } - // Add more entries for other buttons if needed - } - ``` - * Ensure this file contains valid JSON. - ## Running the Service 1. **Build the Docker Image:** @@ -112,7 +89,7 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for This command runs the container in detached mode (`-d`), names it, connects it to the `traefik` network, passes environment variables from the `.env` file, applies the Traefik labels from the `labels` file, and mounts the `subscriptions.json` file into the container. ```bash - docker run -d --name flic-webhook-webpush \ + docker run --rm -d --name flic-webhook-webpush \ --network traefik \ --env-file .env \ --label-file labels \ @@ -136,15 +113,43 @@ In your Flic app or Flic Hub SDK interface: 1. Select your Flic button. 2. Add an "Internet Request" action. 3. Fill in the following details: - * Set `GET` method. + * Select the `GET` method. * Set URL with query parameter: `https:///webhook/SingleClick` (Replace `` with your actual service domain, e.g., `webpush.virtonline.eu`). * **If Basic Authentication is enabled:** - * Set the `Username` and `Password` fields to the values from your `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables. - * **If Basic Authentication is disabled:** - * Leave the `Username` and `Password` fields empty. - * Tap on `Save action`. -4. Repeat for Double Click and/or Hold events, changing the `click_type` parameter accordingly (e.g., `/DoubleClick`). + * Set the Headers: + * Set the `Key` fields to `Authorization`. + * Set the `Value` fields to `Basic `. + * Click `ADD`. + * Tap on `SAVE ACTION`. +4. Repeat for Double Click (i.e., `/DoubleClick`) and Hold (i.e., `/Hold`) events. + The request for the Hold event should look like this: +Flic Button Request + +## App Example: "HTTP Shortcuts" by waboodoo + +Search the Play Store - there might be others with similar names. + +1. Install the App: Download and install "HTTP Shortcuts" or a similar app from the Google Play Store. +2. Create a New Shortcut within the App: + * Open the app and usually tap a '+' or 'Add' button. + * Give your shortcut a Name (e.g., "Turn on Office Light", "Log Water Intake"). + * Choose an Icon. + * Enter the URL you want the request sent to (your webhook URL, IFTTT URL, Home Assistant webhook trigger, etc.). + * Select the HTTP Method (GET, POST, PUT, DELETE, etc. - often GET or POST for simple triggers). + * For POST/PUT: You'll likely need to configure the Request Body (e.g., JSON data) and Content Type (e.g., application/json). + * Authentication: Configure Basic Auth, Bearer Tokens, or custom Headers if your endpoint requires authentication. + * Other Options: Explore settings for response handling (show message on success/failure), timeouts, etc. + * Save the shortcut configuration within the app. +3. Add the Widget/Shortcut to your Home Screen: + * Go to your Android Home Screen. + * Long-press on an empty space. + * Select "Widgets". + * Scroll through the list to find the "HTTP Shortcuts" app (or the app you installed). + * Drag the app's widget or shortcut option onto your home screen. + * The app will likely ask you to choose which specific shortcut (the one you just created) this widget should trigger. Select it. +4. Test: Tap the newly created button on your home screen. It should trigger the internet request you configured. + ## API Endpoints * **`POST /subscribe`** @@ -183,13 +188,12 @@ In your Flic app or Flic Hub SDK interface: * `Timestamp`: Timestamp of the button event (sent by the Flic system). * `Button-Battery-Level`: The battery level percentage of the button (sent by the Flic system). * **Push Notification Payload (`data` field):** The service sends a JSON payload within the push notification. The client-side Service Worker can access this data via `event.data.json()`. The structure is: - ```json - { - "action": "SingleClick", // or DoubleClick, Hold - "button": "game-button", // Normalized button name (lowercase) - "timestamp": "2024-03-28T15:00:00.000Z", // ISO 8601 timestamp or current server time - "batteryLevel": 100 // Integer percentage (0-100) or 'N/A' if not provided - } + ```bash + curl -X GET https://webpush.virtonline.eu/webhook/SingleClick \ + -H 'Authorization: Basic cGxheWVyOlNldmVuT2ZOaW5l' \ + -H "Button-Name: Game-button" \ + -H "Timestamp: 2025-03-26T01:10:20Z" \ + -H "Button-Battery-Level: 100" ``` * **Responses:** * `200 OK`: Webhook received, push notification sent successfully. @@ -203,25 +207,14 @@ In your Flic app or Flic Hub SDK interface: Once your service is up and running, you can test the webhook endpoint using curl or any API testing tool. This example assumes Basic Authentication is enabled. -**Note:** Replace ``, ``, ``, and `` with your actual values. The `Button-Name` header is optional and will default to the value of `DEFAULT_BUTTON_NAME` if not provided. +**Note:** Replace ``, `` with your actual values. The `Button-Name` header is optional and will default to the value of `DEFAULT_BUTTON_NAME` if not provided. ```bash -# Generate Base64 credentials (run this once) -# echo -n ':' | base64 - -# Example using generated Base64 string (replace YOUR_BASE64_CREDS) -curl -X GET "https:///webhook/SingleClick" \ - -H "Authorization: Basic YOUR_BASE64_CREDS" \ - -H "Button-Name: " \ - -H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - -H "Button-Battery-Level: 100" \ - -# Example using curl's built-in Basic Auth (-u) -curl -X GET "https:///webhook/SingleClick" \ - -u ":" \ - -H "Button-Name: " \ - -H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - -H "Button-Battery-Level: 100" \ +curl -X GET "https://webpush.virtonline.eu/webhook/SingleClick" \ + -H "Authorization: Basic $(echo -n 'user:password' | base64)" \ + -H "Button-Name: game-button" \ + -H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -H "Button-Battery-Level: 100" ``` The expected response should be: @@ -231,7 +224,7 @@ The expected response should be: If successful, the above response indicates that: 1. Your webhook endpoint is properly configured -2. The button ID was found in your subscriptions.json file +2. The button name was found in your subscriptions.json file 3. The web push notification was successfully sent to the registered PUSH API endpoint (e.g. https://jmt17.google.com/fcm/send/cf907M...) If you receive a different response, refer to the Troubleshooting section below. @@ -245,4 +238,10 @@ If you receive a different response, refer to the Troubleshooting section below. * **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup. * **Verify `subscriptions.json`:** Ensure it's valid JSON and the button serial number (key) matches exactly what Flic sends (check backend logs for "Received webhook: Button=..."). Check if the subscription details are correct. Case sensitivity matters for the JSON keys (button serials). * **Check Flic Configuration:** Ensure the URL, Method, `click_type` parameter, and authentication details (Username/Password if enabled) are correct in the Flic action setup. Use `curl` or Postman to test the endpoint manually first. -* **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`. \ No newline at end of file +* **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`. +* **Push Notification Retry Mechanism:** The service includes an optimized retry mechanism for handling temporary DNS resolution issues: + * First retry happens immediately or with minimal delay (controlled by `NOTIFICATION_FIRST_RETRY_DELAY_MS`, default 10ms) + * Subsequent retries use exponential backoff with jitter (starting from `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`, default 1000ms) + * Maximum number of retries is controlled by `NOTIFICATION_MAX_RETRIES` (default 3) + * This approach minimizes latency for transient DNS issues while preventing excessive requests for persistent problems + * Adjust these values in your `.env` file based on your network conditions and reliability requirements diff --git a/images/flic-button-request.png b/images/flic-button-request.png index b7ff5ce..0f609d9 100644 Binary files a/images/flic-button-request.png and b/images/flic-button-request.png differ diff --git a/labels b/labels.example similarity index 93% rename from labels rename to labels.example index 355f0f0..b01410c 100644 --- a/labels +++ b/labels.example @@ -17,9 +17,9 @@ traefik.http.routers.flic-webhook-webpush.service=flic-webhook-webpush traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000 # Middleware CORS -traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET,OPTIONS +traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET traefik.http.middlewares.cors-headers.headers.accesscontrolalloworiginlist=https://game-timer.virtonline.eu -traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization +traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization,button-name,button-battery-level,timestamp traefik.http.middlewares.cors-headers.headers.accesscontrolallowcredentials=true traefik.http.middlewares.cors-headers.headers.accesscontrolmaxage=600 traefik.http.middlewares.cors-headers.headers.addvaryheader=true diff --git a/package-lock.json b/package-lock.json index f0ce88d..4960405 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "web-push": "^3.6.7" @@ -151,18 +150,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -614,14 +601,6 @@ "node": ">= 0.6" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index 81120fa..a1e453d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "author": "Your Name", "license": "MIT", "dependencies": { - "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "web-push": "^3.6.7" diff --git a/server.js b/server.js index 775967c..fc24ffe 100644 --- a/server.js +++ b/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()}`); diff --git a/virt-flic-webhook-webpush.service.example b/virt-flic-webhook-webpush.service.example index c9fd808..4bc8444 100644 --- a/virt-flic-webhook-webpush.service.example +++ b/virt-flic-webhook-webpush.service.example @@ -7,20 +7,18 @@ DefaultDependencies=no [Service] Type=simple Environment="HOME=/root" -Environment="APP_PATH=/virt/flic-webhook-webpush" - ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true' ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-flic-webhook-webpush 2>/dev/null || true' -ExecStartPre=/usr/bin/env sh -c 'touch ${APP_PATH}/subscriptions.json' +ExecStartPre=/usr/bin/env sh -c 'touch /virt/flic-webhook-webpush/subscriptions.json' ExecStart=/usr/bin/env docker run \ --rm \ --name=virt-flic-webhook-webpush \ --log-driver=none \ --network=traefik \ - --env-file=${APP_PATH}/.env \ - --label-file=${APP_PATH}/labels \ - --mount type=bind,src=${APP_PATH}/subscriptions.json,dst=/app/subscriptions.json \ + --env-file=/virt/flic-webhook-webpush/env \ + --label-file=/virt/flic-webhook-webpush/labels \ + --mount type=bind,src=/virt/flic-webhook-webpush/subscriptions.json,dst=/app/subscriptions.json \ flic-webhook-webpush ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true'