dns retry
This commit is contained in:
@@ -5,4 +5,6 @@ Dockerfile
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
README.md
|
README.md
|
||||||
*.example
|
*.example
|
||||||
|
.env.example
|
||||||
|
.labels.example
|
||||||
|
|||||||
22
.env.example
22
.env.example
@@ -10,11 +10,6 @@ VAPID_PRIVATE_KEY=
|
|||||||
# Example: mailto:admin@yourdomain.com or https://yourdomain.com/contact
|
# Example: mailto:admin@yourdomain.com or https://yourdomain.com/contact
|
||||||
VAPID_SUBJECT=mailto:admin@virtonline.eu
|
VAPID_SUBJECT=mailto:admin@virtonline.eu
|
||||||
|
|
||||||
# Flic Button Configuration
|
|
||||||
FLIC_BUTTON1_SERIAL=your_button1_serial
|
|
||||||
FLIC_BUTTON2_SERIAL=your_button2_serial
|
|
||||||
FLIC_BUTTON3_SERIAL=your_button3_serial
|
|
||||||
|
|
||||||
# Subscription Storage
|
# Subscription Storage
|
||||||
SUBSCRIPTIONS_FILE=subscriptions.json
|
SUBSCRIPTIONS_FILE=subscriptions.json
|
||||||
|
|
||||||
@@ -30,4 +25,19 @@ LOG_LEVEL=INFO
|
|||||||
# If you want to add a simple security layer between Flic and this app.
|
# If you want to add a simple security layer between Flic and this app.
|
||||||
# If set, configure Flic's HTTP request to include an "Authorization: Bearer YOUR_SECRET_VALUE" header.
|
# If set, configure Flic's HTTP request to include an "Authorization: Bearer YOUR_SECRET_VALUE" header.
|
||||||
# use e.g.: openssl rand -hex 32
|
# use e.g.: openssl rand -hex 32
|
||||||
FLIC_SECRET=
|
FLIC_SECRET=
|
||||||
|
|
||||||
|
# --- DNS and Network Configuration ---
|
||||||
|
# These settings help with Docker DNS resolution issues (EAI_AGAIN errors)
|
||||||
|
|
||||||
|
# Maximum number of retry attempts for failed DNS resolutions
|
||||||
|
MAX_NOTIFICATION_RETRIES=3
|
||||||
|
|
||||||
|
# Initial delay in milliseconds before first retry (will increase with backoff)
|
||||||
|
INITIAL_RETRY_DELAY_MS=1000
|
||||||
|
|
||||||
|
# DNS resolution timeout in milliseconds
|
||||||
|
DNS_TIMEOUT_MS=5000
|
||||||
|
|
||||||
|
# HTTP request timeout in milliseconds
|
||||||
|
HTTP_TIMEOUT_MS=10000
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
myenv
|
myenv
|
||||||
.vscode
|
.vscode
|
||||||
|
.cursor
|
||||||
|
|
||||||
# Node.js
|
# Node.js
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -8,7 +9,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.env
|
.env
|
||||||
subscriptions.json
|
subscriptions.json
|
||||||
labels
|
|
||||||
|
|
||||||
# OS generated files
|
# OS generated files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
80
README.md
80
README.md
@@ -7,8 +7,9 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Receives POST requests on `/flic-webhook`.
|
* Receives POST requests on `/flic-webhook`.
|
||||||
* Parses `button_id` and `click_type` from the Flic request body.
|
* Uses HTTP headers `Button-Name` and `Timestamp` from the Flic request.
|
||||||
* Looks up the target PWA push subscription based on `button_id` in a JSON file.
|
* Parses `click_type` from the Flic request body.
|
||||||
|
* Looks up the target PWA push subscription based on the `Button-Name` header in a JSON file.
|
||||||
* Sends a Web Push notification containing the click details (action, button, timestamp) to the corresponding PWA subscription.
|
* Sends a Web Push notification containing the click details (action, button, timestamp) to the corresponding PWA subscription.
|
||||||
* Integrates with Traefik v3 via Docker labels.
|
* Integrates with Traefik v3 via Docker labels.
|
||||||
* Configurable via environment variables (`.env` file).
|
* Configurable via environment variables (`.env` file).
|
||||||
@@ -27,10 +28,10 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
## System Architecture
|
## System Architecture
|
||||||
|
|
||||||
### Subscription Flow
|
### Subscription Flow
|
||||||

|
<img src="images/subscription-flow.png" width="600" alt="Subscription Flow">
|
||||||
|
|
||||||
### Interaction Flow
|
### Interaction Flow
|
||||||

|
<img src="images/interaction-flow.png" width="300" alt="Interaction Flow">
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -64,6 +65,10 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
* `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_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_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed.
|
||||||
* `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers 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.
|
||||||
|
* `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.
|
||||||
* `TRAEFIK_SERVICE_HOST`: Your public domain for this service (e.g., `webpush.virtonline.eu`).
|
* `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`).
|
* `TRAEFIK_CERT_RESOLVER`: The name of your TLS certificate resolver configured in Traefik (e.g., `le`, `myresolver`).
|
||||||
|
|
||||||
@@ -134,25 +139,27 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
|
|||||||
In your Flic app or Flic Hub SDK interface:
|
In your Flic app or Flic Hub SDK interface:
|
||||||
|
|
||||||
1. Select your Flic button.
|
1. Select your Flic button.
|
||||||
2. Add an "Internet Request" action (or similar HTTP request action) for Single Click, Double Click, and/or Hold events.
|
2. Add an "Internet Request" action.
|
||||||
3. **URL:** `https://<YOUR_TRAEFIK_SERVICE_HOST>/flic-webhook` (e.g., `https://webpush.virtonline.eu/flic-webhook`)
|
3. Fill in the following details:
|
||||||
4. **Method:** `POST`
|
* Set `POST` method.
|
||||||
5. **Body Type:** `JSON` (or `application/json`)
|
* Set URL: `https://webpush.virtonline.eu/flic-webhook`
|
||||||
6. **Body:** Configure the JSON body to include the button's serial number and the click type. Flic usually provides variables for these. The backend expects `button_id` and `click_type`. Adapt the keys if needed, or modify `server.js` to expect different keys (e.g., `serialNumber`).
|
* Add headers:
|
||||||
|
* Key: `Authorization`
|
||||||
|
* Value: `Bearer <FLIC_SECRET>` (Replace `<FLIC_SECRET>` with the actual secret from your `.env` file).
|
||||||
|
* **Body:** Configure the JSON body to include the click type.
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"button_id": "{serialNumber}",
|
"click_type": "SingleClick"
|
||||||
"click_type": "{clickType}",
|
|
||||||
"timestamp": "{timestamp}"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*(Verify the exact variable names like `{serialNumber}`, `{clickType}`, `{timestamp}` within your specific Flic interface.)*
|
* Set Content-Type: `application/json`.
|
||||||
7. **Headers:**
|
* It should look like this:
|
||||||
* Add `Content-Type: application/json`.
|
|
||||||
* **(Optional - if `FLIC_SECRET` is set):** Add an `Authorization` header:
|
|
||||||
* Key: `Authorization`
|
|
||||||
* Value: `Bearer <YOUR_FLIC_SECRET_VALUE>` (Replace `<YOUR_FLIC_SECRET_VALUE>` with the actual secret from your `.env` file).
|
|
||||||
|
|
||||||
|
<!-- <img src="images/flic-button-request.png" width="300" alt="Flic Button Request Configuration"> // TODO: Update image -->
|
||||||
|
|
||||||
|
* Tap on `Save action`.
|
||||||
|
4. Repeat for Double Click and/or Hold events.
|
||||||
|
|
||||||
## API Endpoint
|
## API Endpoint
|
||||||
|
|
||||||
* **`POST /flic-webhook`**
|
* **`POST /flic-webhook`**
|
||||||
@@ -161,16 +168,14 @@ In your Flic app or Flic Hub SDK interface:
|
|||||||
* **Request Body (JSON):**
|
* **Request Body (JSON):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"button_id": "SERIAL_NUMBER_OF_FLIC_BUTTON",
|
"click_type": "SingleClick | DoubleClick | Hold"
|
||||||
"click_type": "SingleClick | DoubleClick | Hold",
|
|
||||||
"timestamp": "ISO_8601_TIMESTAMP_STRING (Optional)"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
* **Responses:**
|
* **Responses:**
|
||||||
* `200 OK`: Webhook received, push notification sent successfully.
|
* `200 OK`: Webhook received, push notification sent successfully.
|
||||||
* `400 Bad Request`: Missing `button_id` or `click_type` in the request body.
|
* `400 Bad Request`: Missing `Button-Name` header or `click_type` in the request body.
|
||||||
* `401 Unauthorized`: Missing or invalid Bearer token (if `FLIC_SECRET` is enabled).
|
* `401 Unauthorized`: Missing or invalid Bearer token (if `FLIC_SECRET` is enabled).
|
||||||
* `404 Not Found`: No subscription found in `subscriptions.json` for the given `button_id`.
|
* `404 Not Found`: No subscription found in `subscriptions.json` for the given `Button-Name`.
|
||||||
* `410 Gone`: The push subscription associated with the button was rejected by the push service (likely expired or revoked).
|
* `410 Gone`: The push subscription associated with the button was rejected by the push service (likely expired or revoked).
|
||||||
* `500 Internal Server Error`: Failed to send the push notification for other reasons.
|
* `500 Internal Server Error`: Failed to send the push notification for other reasons.
|
||||||
|
|
||||||
@@ -184,6 +189,35 @@ In your Flic app or Flic Hub SDK interface:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Testing the Webhook
|
||||||
|
|
||||||
|
Once your service is up and running, you can test the webhook endpoint using curl or any API testing tool:
|
||||||
|
|
||||||
|
**Note:** In the command below, replace `a74181969a613c545d66c1e436e75c1e4a6` with your actual FLIC_SECRET value from your .env file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://webpush.virtonline.eu/flic-webhook \
|
||||||
|
-H "Authorization: Bearer a74181969a613c545d66c1e436e75c1e4a6" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Button-Name: Game" \
|
||||||
|
-H "Timestamp: 2025-03-26T01:10:20Z" \
|
||||||
|
-d '{
|
||||||
|
"click_type": "SingleClick"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The expected response should be:
|
||||||
|
```json
|
||||||
|
{"message":"Push notification sent successfully"}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
* **Check Backend Logs:** `docker logs flic-webhook-webpush`. Look for errors related to configuration, file access, JSON parsing, authentication, or sending push notifications.
|
* **Check Backend Logs:** `docker logs flic-webhook-webpush`. Look for errors related to configuration, file access, JSON parsing, authentication, or sending push notifications.
|
||||||
|
|||||||
BIN
images/flic-button-request.png
Normal file
BIN
images/flic-button-request.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
32
labels
Normal file
32
labels
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Enable Traefik for this container
|
||||||
|
traefik.enable=true
|
||||||
|
|
||||||
|
# Docker Network
|
||||||
|
traefik.docker.network=traefik
|
||||||
|
|
||||||
|
# Route requests based on Host
|
||||||
|
traefik.http.routers.flic-webhook-webpush.rule=Host(`webpush.virtonline.eu`)
|
||||||
|
# Specify the entrypoint ('websecure' for HTTPS)
|
||||||
|
traefik.http.routers.flic-webhook-webpush.entrypoints=web-secure
|
||||||
|
traefik.http.routers.flic-webhook-webpush.tls=true # Enable TLS
|
||||||
|
traefik.http.routers.flic-webhook-webpush.tls.certResolver=default
|
||||||
|
# Link the router to the service defined below
|
||||||
|
traefik.http.routers.flic-webhook-webpush.service=flic-webhook-webpush
|
||||||
|
|
||||||
|
# Point the service to the container's port
|
||||||
|
traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000
|
||||||
|
|
||||||
|
# Middleware CORS
|
||||||
|
traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET,OPTIONS # Allow POST, GET, and OPTIONS requests
|
||||||
|
traefik.http.middlewares.cors-headers.headers.accesscontrolalloworiginlist=https://game-timer.virtonline.eu # Allow requests from game-timer.virtonline.eu
|
||||||
|
traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization # Allow Content-Type and Authorization headers
|
||||||
|
traefik.http.middlewares.cors-headers.headers.accesscontrolmaxage=600 # Cache preflight responses for 10 minutes
|
||||||
|
traefik.http.middlewares.cors-headers.headers.addvaryheader=true # Add Vary header to responses
|
||||||
|
# Apply the middleware to the router
|
||||||
|
traefik.http.routers.flic-webhook-webpush.middlewares=cors-headers
|
||||||
|
|
||||||
|
# Middleware Rate Limiting
|
||||||
|
traefik.http.middlewares.flic-ratelimit.ratelimit.average=10 # requests per second
|
||||||
|
traefik.http.middlewares.flic-ratelimit.ratelimit.burst=20
|
||||||
|
# Apply the middleware to the router
|
||||||
|
traefik.http.routers.flic-webhook-webpush.middlewares=flic-ratelimit
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Traefik v3 Labels for flic-webhook-webpush service
|
|
||||||
|
|
||||||
# Enable Traefik for this container
|
|
||||||
traefik.enable=true
|
|
||||||
|
|
||||||
# --- HTTP Router Definition ---
|
|
||||||
# Define an HTTP router named 'flic-webhook-http'
|
|
||||||
# Route requests based on Host and PathPrefix
|
|
||||||
traefik.http.routers.flic-webhook.rule=Host(`webpush.virtonline.eu`)
|
|
||||||
# Specify the entrypoint (e.g., 'websecure' for HTTPS)
|
|
||||||
traefik.http.routers.flic-webhook.entrypoints=websecure
|
|
||||||
# Specify the TLS certificate resolver
|
|
||||||
traefik.http.routers.flic-webhook.tls.certresolver=default
|
|
||||||
# Link this router to the service defined below
|
|
||||||
traefik.http.routers.flic-webhook.service=flic-webhook
|
|
||||||
|
|
||||||
# --- HTTP Service Definition ---
|
|
||||||
# Define an HTTP service named 'flic-webhook'
|
|
||||||
# Point the service to the container's port (default 3000)
|
|
||||||
traefik.http.services.flic-webhook.loadbalancer.server.port=3000
|
|
||||||
|
|
||||||
# --- Middleware (Optional Example: Rate Limiting - Uncomment to enable) ---
|
|
||||||
# traefik.http.middlewares.flic-ratelimit.ratelimit.average=10 # requests per second
|
|
||||||
# traefik.http.middlewares.flic-ratelimit.ratelimit.burst=20
|
|
||||||
# traefik.http.routers.flic-webhook.middlewares=flic-ratelimit
|
|
||||||
|
|
||||||
# --- Docker Network ---
|
|
||||||
# Ensure Traefik uses the correct network to communicate with the container
|
|
||||||
traefik.docker.network=traefik
|
|
||||||
104
server.js
104
server.js
@@ -3,6 +3,7 @@ const webpush = require('web-push');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const dns = require('dns'); // Add DNS module
|
||||||
|
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
@@ -19,7 +20,26 @@ const flicSecret = process.env.FLIC_SECRET; // Optional Bearer token secret for
|
|||||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin => origin.trim()).filter(origin => origin);
|
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 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);
|
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
|
||||||
|
// HTTP request timeout configuration
|
||||||
|
const httpTimeout = parseInt(process.env.HTTP_TIMEOUT_MS || 10000, 10); // 10 seconds
|
||||||
|
|
||||||
|
// Configure global HTTP agent with timeouts to prevent hanging requests
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// Custom HTTPS agent with timeout
|
||||||
|
const httpsAgent = new https.Agent({
|
||||||
|
keepAlive: true,
|
||||||
|
timeout: httpTimeout,
|
||||||
|
maxSockets: 50, // Limit concurrent connections
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the agent to the webpush module if possible
|
||||||
|
// Note: The web-push library might use its own agent, but this is a precaution
|
||||||
|
https.globalAgent = httpsAgent;
|
||||||
|
|
||||||
// --- Validation ---
|
// --- Validation ---
|
||||||
if (!vapidPublicKey || !vapidPrivateKey || !vapidSubject) {
|
if (!vapidPublicKey || !vapidPrivateKey || !vapidSubject) {
|
||||||
@@ -34,6 +54,43 @@ webpush.setVapidDetails(
|
|||||||
vapidPrivateKey
|
vapidPrivateKey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Configure DNS settings for more reliable resolution in containerized environments
|
||||||
|
// These settings can help with temporary DNS resolution failures
|
||||||
|
dns.setDefaultResultOrder('ipv4first'); // Prefer IPv4 to avoid some IPv6 issues in containers
|
||||||
|
const dnsTimeout = parseInt(process.env.DNS_TIMEOUT_MS || 5000, 10);
|
||||||
|
dns.setServers(dns.getServers()); // Reset DNS servers (can help in some Docker environments)
|
||||||
|
|
||||||
|
// You can optionally configure a specific DNS server if needed:
|
||||||
|
// 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) {
|
||||||
|
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' ||
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Subscription Loading and Management ---
|
// --- Subscription Loading and Management ---
|
||||||
let subscriptions = {}; // In-memory cache of subscriptions
|
let subscriptions = {}; // In-memory cache of subscriptions
|
||||||
|
|
||||||
@@ -159,51 +216,54 @@ app.post('/subscribe', async (req, res) => {
|
|||||||
// --- Flic Webhook Endpoint ---
|
// --- Flic Webhook Endpoint ---
|
||||||
// Apply Flic-specific authentication ONLY to this route
|
// Apply Flic-specific authentication ONLY to this route
|
||||||
app.post('/flic-webhook', authenticateFlicRequest, async (req, res) => {
|
app.post('/flic-webhook', authenticateFlicRequest, async (req, res) => {
|
||||||
// Assuming Flic sends 'button_id' which is the serial number
|
// Get buttonName from Header 'Button-Name' and timestamp from Header 'Timestamp'
|
||||||
const { button_id, click_type, timestamp } = req.body;
|
const buttonName = req.headers['button-name'];
|
||||||
|
const timestamp = req.headers['timestamp'];
|
||||||
|
// Still get click_type from the request body
|
||||||
|
const { click_type } = req.body;
|
||||||
|
|
||||||
console.log(`Received webhook: Button=${button_id}, Type=${click_type}, Timestamp=${timestamp || 'N/A'}`);
|
console.log(`Received webhook: Button=${buttonName}, Type=${click_type}, Timestamp=${timestamp || 'N/A'}`);
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!button_id || !click_type) {
|
if (!buttonName || !click_type) {
|
||||||
console.warn(`Webhook Error: Missing button_id or click_type`);
|
console.warn(`Webhook Error: Missing Button-Name header or click_type in body`);
|
||||||
return res.status(400).json({ message: 'Bad Request: Missing button_id or click_type' });
|
return res.status(400).json({ message: 'Bad Request: Missing Button-Name header or click_type in request body' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedButtonId = button_id.toLowerCase(); // Use lowercase for lookup consistency
|
const normalizedButtonName = buttonName.toLowerCase(); // Use lowercase for lookup consistency
|
||||||
|
|
||||||
// Find the subscription associated with this button ID
|
// Find the subscription associated with this normalized button name
|
||||||
const subscription = subscriptions[normalizedButtonId];
|
const subscription = subscriptions[normalizedButtonName];
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
console.warn(`Webhook: No subscription found for button ID: ${normalizedButtonId} (original: ${button_id})`);
|
console.warn(`Webhook: No subscription found for button ID: ${normalizedButtonName} (original: ${buttonName})`);
|
||||||
return res.status(404).json({ message: `Not Found: No subscription configured for button ${normalizedButtonId}` });
|
return res.status(404).json({ message: `Not Found: No subscription configured for button ${normalizedButtonName}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Send Web Push Notification ---
|
// --- Send Web Push Notification ---
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
title: 'Flic Button Action',
|
title: 'Flic Button Action',
|
||||||
body: `Button ${click_type}`, // Simplified body
|
body: `Button ${normalizedButtonName}: ${click_type}`, // Simplified body
|
||||||
data: {
|
data: {
|
||||||
action: click_type,
|
action: click_type,
|
||||||
button: normalizedButtonId, // Send normalized ID
|
button: normalizedButtonName, // Send normalized button name
|
||||||
timestamp: timestamp || new Date().toISOString()
|
timestamp: timestamp || new Date().toISOString()
|
||||||
}
|
}
|
||||||
// icon: '/path/to/icon.png'
|
// icon: '/path/to/icon.png'
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`Sending push notification for ${normalizedButtonId} to endpoint: ${subscription.endpoint.substring(0, 40)}...`);
|
console.log(`Sending push notification for ${normalizedButtonName} to endpoint: ${subscription.endpoint.substring(0, 40)}...`);
|
||||||
await webpush.sendNotification(subscription, payload);
|
await sendWebPushWithRetry(subscription, payload);
|
||||||
console.log(`Push notification sent successfully for button ${normalizedButtonId}.`);
|
console.log(`Push notification sent successfully for button ${normalizedButtonName}.`);
|
||||||
res.status(200).json({ message: 'Push notification sent successfully' });
|
res.status(200).json({ message: 'Push notification sent successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error sending push notification for button ${normalizedButtonId}:`, error.body || error.message || error);
|
console.error(`Error sending push notification for button ${normalizedButtonName}:`, error.body || error.message || error);
|
||||||
|
|
||||||
if (error.statusCode === 404 || error.statusCode === 410) {
|
if (error.statusCode === 404 || error.statusCode === 410) {
|
||||||
console.warn(`Subscription for button ${normalizedButtonId} is invalid or expired (404/410). Removing it.`);
|
console.warn(`Subscription for button ${normalizedButtonName} is invalid or expired (404/410). Removing it.`);
|
||||||
// Optionally remove the stale subscription
|
// Optionally remove the stale subscription
|
||||||
delete subscriptions[normalizedButtonId];
|
delete subscriptions[normalizedButtonName];
|
||||||
saveSubscriptions(); // Attempt to save the updated list
|
saveSubscriptions(); // Attempt to save the updated list
|
||||||
res.status(410).json({ message: 'Subscription Gone' });
|
res.status(410).json({ message: 'Subscription Gone' });
|
||||||
} else {
|
} else {
|
||||||
@@ -223,7 +283,6 @@ app.get('/health', (req, res) => {
|
|||||||
|
|
||||||
// --- Start Server ---
|
// --- Start Server ---
|
||||||
// Use http.createServer to allow graceful shutdown
|
// Use http.createServer to allow graceful shutdown
|
||||||
const http = require('http');
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
@@ -234,6 +293,9 @@ server.listen(port, () => {
|
|||||||
console.log(`Flic Webhook Auth: ${flicSecret ? 'Enabled (Bearer Token)' : 'Disabled'}`);
|
console.log(`Flic Webhook Auth: ${flicSecret ? 'Enabled (Bearer Token)' : 'Disabled'}`);
|
||||||
console.log(`Subscription Endpoint Auth: Disabled`);
|
console.log(`Subscription Endpoint Auth: Disabled`);
|
||||||
console.log(`Subscriptions File: ${subscriptionsFilePath}`);
|
console.log(`Subscriptions File: ${subscriptionsFilePath}`);
|
||||||
|
console.log(`Push Notification Retry Config: ${maxRetries} retries, ${initialRetryDelay}ms initial delay`);
|
||||||
|
console.log(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);
|
||||||
|
console.log(`HTTP Timeout: ${httpTimeout}ms`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Graceful Shutdown ---
|
// --- Graceful Shutdown ---
|
||||||
@@ -249,7 +311,7 @@ const closeGracefully = (signal) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.error('Could not close connections in time, forcefully shutting down');
|
console.error('Could not close connections in time, forcefully shutting down');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}, 10000); // 10 seconds timeout
|
}, 10000); // 10 seconds timeout - This is a literal so no need to parse
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('SIGTERM', () => closeGracefully('SIGTERM'));
|
process.on('SIGTERM', () => closeGracefully('SIGTERM'));
|
||||||
|
|||||||
Reference in New Issue
Block a user