Compare commits

..

29 Commits

Author SHA1 Message Date
cpu
675c0a2d87 beter variable names 2025-03-28 21:06:41 +01:00
cpu
1a241e5355 clean up 2025-03-28 20:46:11 +01:00
cpu
43cf8801a0 clean up 2025-03-28 20:38:27 +01:00
cpu
b9199cd964 clean up 2025-03-28 19:49:47 +01:00
cpu
fc25c4a000 labels 2025-03-28 19:21:40 +01:00
cpu
4f76bae187 cleanup 2025-03-28 19:18:32 +01:00
cpu
058441b6e6 default button name 2025-03-28 17:51:25 +01:00
cpu
5a3b4974c4 changed the route name 2025-03-28 17:28:24 +01:00
cpu
fda9178264 fixed cors 2025-03-28 15:45:20 +01:00
cpu
b6f9e07d70 enable debug 2025-03-28 03:11:14 +01:00
cpu
13a16d71f8 added debug for headers received 2025-03-28 02:35:07 +01:00
cpu
4f46c1be99 change GET to POST, removed /health 2025-03-28 02:12:52 +01:00
cpu
228f4984d8 dns retry 2025-03-28 01:47:05 +01:00
cpu
398c6473a4 flow diagrams 2025-03-27 20:17:35 +01:00
cpu
c89eaacd42 auth 2025-03-26 23:57:03 +01:00
cpu
682dc6942a added subscription route 2025-03-26 20:31:31 +01:00
cpu
064f803784 Merge branch 'one_line_key' 2025-03-26 19:22:19 +01:00
cpu
9b4bf1e255 flask and node.js solution 2025-03-26 19:12:43 +01:00
cpu
fc7f4f4b7a rewritten 2025-03-26 09:36:58 +01:00
cpu
b87e30f6b4 fix 2025-03-26 09:25:28 +01:00
cpu
faa32510df again validations 2025-03-26 08:59:03 +01:00
cpu
ce7ab594e2 improved validations 2025-03-26 08:52:30 +01:00
cpu
f500c00896 logs 2025-03-26 08:34:51 +01:00
cpu
b246923283 clean up 2025-03-26 08:21:33 +01:00
cpu
907ad382dc helper prints 2025-03-26 08:16:56 +01:00
cpu
102d2e2748 fixed key to one line 2025-03-26 06:38:33 +01:00
cpu
f2de1e55d0 CORS 2025-03-26 05:33:53 +01:00
cpu
ba9704d3c2 config 2025-03-26 03:57:54 +01:00
cpu
95a5b893ec first version 2025-03-26 03:09:49 +01:00
8 changed files with 163 additions and 132 deletions

39
.env
View File

@@ -1,39 +0,0 @@
# Flic to PWA WebPush Configuration
# --- VAPID Keys (Required) ---
# Generate using: npx web-push generate-vapid-keys
VAPID_PUBLIC_KEY=BKfRJXjSQmAJ452gLwlK_8scGrW6qMU1mBRp39ONtcQHkSsQgmLAaODIyGbgHyRpnDEv3HfXV1oGh3SC0fHxY0E
VAPID_PRIVATE_KEY=ErEgsDKYQi5j2KPERC_gCtrEALAD0k-dWSwrrcD0-JU
VAPID_SUBJECT=mailto:admin@virtonline.eu
# --- Server Configuration ---
# Internal port for the Node.js app
PORT=3000
SUBSCRIPTIONS_FILE=subscriptions.json
DEFAULT_BUTTON_NAME=game-button
# --- Authentication (Optional) ---
# If both USERNAME and PASSWORD are set, Basic Auth will be enabled for:
# - POST /subscribe
# - GET /webhook
# Leave blank to disable authentication.
BASIC_AUTH_USERNAME=player
BASIC_AUTH_PASSWORD=SevenOfNine
# --- Web Push Retry Configuration (Optional) ---
# 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) ---
# 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
LOG_LEVEL=info

View File

@@ -9,7 +9,9 @@ VAPID_SUBJECT=mailto:mailto:user@example.org
# --- Server Configuration --- # --- Server Configuration ---
# Internal port for the Node.js app # Internal port for the Node.js app
PORT=3000 PORT=3000
# Path inside the container
SUBSCRIPTIONS_FILE=/app/subscriptions.json SUBSCRIPTIONS_FILE=/app/subscriptions.json
# Default button name to use when not specified
DEFAULT_BUTTON_NAME=game-button DEFAULT_BUTTON_NAME=game-button
# --- Authentication (Optional) --- # --- Authentication (Optional) ---
@@ -20,6 +22,14 @@ DEFAULT_BUTTON_NAME=game-button
BASIC_AUTH_USERNAME=user12345 BASIC_AUTH_USERNAME=user12345
BASIC_AUTH_PASSWORD=password 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) --- # --- Web Push Retry Configuration (Optional) ---
# Number of retries on failure (e.g., DNS issues) # Number of retries on failure (e.g., DNS issues)
NOTIFICATION_MAX_RETRIES=3 NOTIFICATION_MAX_RETRIES=3

155
README.md
View File

@@ -15,6 +15,7 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
* 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).
* Optional Basic Authentication for securing the `/webhook` and `/subscribe` endpoints. * 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 ## Prerequisites
@@ -51,31 +52,54 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
This will output a Public Key and a Private Key. This will output a Public Key and a Private Key.
3. **Configure Environment Variables:** 3. **Configure Environment Variables:**
* Copy the example `.env` file: * Copy the example `.env` file:
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
* Edit the `.env` file with your specific values: * Edit the `.env` file with your specific values:
* `VAPID_PUBLIC_KEY`: The public key generated in step 2. **Your PWA will also need this key** when it subscribes to push notifications. * `VAPID_PUBLIC_KEY`: The public key generated in step 2. **Your PWA will also need this key** when it subscribes to push notifications.
* `VAPID_PRIVATE_KEY`: The private key generated in step 2. **Keep this secret!** * `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. * `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. * `PORT`: (Default: `3000`) The internal port the Node.js app listens on. Traefik will map to this.
* `SUBSCRIPTIONS_FILE`: (Default: `subscriptions.json`) The path *inside the container* where the button-to-subscription mapping is stored. * `SUBSCRIPTIONS_FILE`: (Default: `/app/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. * `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_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. * `BASIC_AUTH_PASSWORD`: (Optional) Password for Basic Authentication. If set along with `BASIC_AUTH_USERNAME`, authentication will be enabled.
* `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number. * `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`.
* `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. * `ALLOWED_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed.
* `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. * `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed.
* `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number. * `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number.
* `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. 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.
* `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. * `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:** 4. **Configure Traefik Labels:**
* Copy the example `labels` file: * Copy the example `labels` file:
```bash ```bash
cp labels.example labels 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 ## Running the Service
@@ -89,7 +113,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. 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 ```bash
docker run --rm -d --name flic-webhook-webpush \ docker run -d --name flic-webhook-webpush \
--network traefik \ --network traefik \
--env-file .env \ --env-file .env \
--label-file labels \ --label-file labels \
@@ -113,42 +137,14 @@ 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. 2. Add an "Internet Request" action.
3. Fill in the following details: 3. Fill in the following details:
* Select the `GET` method. * Set `GET` method.
* Set URL with query parameter: `https://webpush.virtonline.eu/webhook/SingleClick` * Set URL with query parameter: `https://<your_domain>/webhook/SingleClick` (Replace `<your_domain>` with your actual service domain, e.g., `webpush.virtonline.eu`).
* **If Basic Authentication is enabled:** * **If Basic Authentication is enabled:**
* Set the Headers: * Set the `Username` and `Password` fields to the values from your `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables.
* Set the `Key` fields to `Authorization`. * **If Basic Authentication is disabled:**
* Set the `Value` fields to `Basic <base64 encoded username:password>` (e.g., `Basic dXNlcm5hbWU6cGFzc3dvcmQ=`). Use `$(echo -n 'user:password' | base64)` to generate the base64 encoded string. * Leave the `Username` and `Password` fields empty.
* Click `ADD`. * Tap on `Save action`.
* Tap on `SAVE ACTION`. 4. Repeat for Double Click and/or Hold events, changing the `click_type` parameter accordingly (e.g., `/DoubleClick`).
4. Repeat for Double Click (i.e., `/DoubleClick`) and Hold (i.e., `/Hold`) events.
The request for the Hold event should look like this:
<img src="images/flic-button-request.png" width="300" alt="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 ## API Endpoints
@@ -160,7 +156,7 @@ Search the Play Store - there might be others with similar names.
* `subscription` (object, required): The [PushSubscription object](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) obtained from the browser's Push API. * `subscription` (object, required): The [PushSubscription object](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) obtained from the browser's Push API.
```json ```json
{ {
"button_id": "game-button", "button_id": "game-button", // Optional, defaults to DEFAULT_BUTTON_NAME environment variable
"subscription": { "subscription": {
"endpoint": "https://your_pwa_push_endpoint...", "endpoint": "https://your_pwa_push_endpoint...",
"expirationTime": null, "expirationTime": null,
@@ -182,10 +178,20 @@ Search the Play Store - there might be others with similar names.
* **Authentication:** Optional Basic Authentication via `Authorization` header if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are configured. * **Authentication:** Optional Basic Authentication via `Authorization` header if `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` are configured.
* **URL Parameters:** * **URL Parameters:**
* `click_type` (required): The type of button press (e.g., `SingleClick`, `DoubleClick`, or `Hold`). * `click_type` (required): The type of button press (e.g., `SingleClick`, `DoubleClick`, or `Hold`).
* **Optional Headers:** * **Required Headers:**
* `Button-Name`: The identifier of the Flic button (sent by the Flic system). If not provided, the value of `DEFAULT_BUTTON_NAME` environment variable will be used as a fallback. * `Button-Name`: The identifier of the Flic button (sent by the Flic system). If not provided, the value of `DEFAULT_BUTTON_NAME` environment variable will be used as a fallback.
* **Optional Headers:**
* `Timestamp`: Timestamp of the button event (sent by the Flic system). * `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). * `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
}
```
* **Responses:** * **Responses:**
* `200 OK`: Webhook received, push notification sent successfully. * `200 OK`: Webhook received, push notification sent successfully.
* `400 Bad Request`: Missing `Button-Name` header or `click_type` URL parameter. * `400 Bad Request`: Missing `Button-Name` header or `click_type` URL parameter.
@@ -198,14 +204,25 @@ Search the Play Store - there might be others with similar names.
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. 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 `<username>`, `<password>` 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 `<username>`, `<password>`, `<your_domain>`, and `<button_name>` with your actual values. The `Button-Name` header is optional and will default to the value of `DEFAULT_BUTTON_NAME` if not provided.
```bash ```bash
curl -X GET "https://webpush.virtonline.eu/webhook/SingleClick" \ # Generate Base64 credentials (run this once)
-H "Authorization: Basic $(echo -n 'user:password' | base64)" \ # echo -n '<username>:<password>' | base64
-H "Button-Name: game-button" \
-H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \ # Example using generated Base64 string (replace YOUR_BASE64_CREDS)
-H "Button-Battery-Level: 100" curl -X GET "https://<your_domain>/webhook/SingleClick" \
-H "Authorization: Basic YOUR_BASE64_CREDS" \
-H "Button-Name: <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://<your_domain>/webhook/SingleClick" \
-u "<username>:<password>" \
-H "Button-Name: <button_name>" \
-H "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-H "Button-Battery-Level: 100" \
``` ```
The expected response should be: The expected response should be:
@@ -215,7 +232,7 @@ The expected response should be:
If successful, the above response indicates that: If successful, the above response indicates that:
1. Your webhook endpoint is properly configured 1. Your webhook endpoint is properly configured
2. The button name was found in your subscriptions.json file 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...) 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. If you receive a different response, refer to the Troubleshooting section below.
@@ -223,7 +240,7 @@ 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.
* To see detailed debug information including all headers received from the Flic button, set `LOG_LEVEL=debug` in your .env file. * To see detailed debug information including all headers received from the Flic button, set `LOG_LEVEL=debug` in your .env file.
* **Check Traefik Logs:** `docker logs traefik`. Look for routing errors or certificate issues. * **Check Traefik Logs:** `docker logs traefik`. Look for routing errors or certificate issues.
* **Verify `.env`:** Ensure all required variables are set correctly, especially VAPID keys and Traefik settings. * **Verify `.env`:** Ensure all required variables are set correctly, especially VAPID keys and Traefik settings.
* **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup. * **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -17,9 +17,9 @@ traefik.http.routers.flic-webhook-webpush.service=flic-webhook-webpush
traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000 traefik.http.services.flic-webhook-webpush.loadbalancer.server.port=3000
# Middleware CORS # Middleware CORS
traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=POST,GET,OPTIONS
traefik.http.middlewares.cors-headers.headers.accesscontrolalloworiginlist=https://game-timer.virtonline.eu traefik.http.middlewares.cors-headers.headers.accesscontrolalloworiginlist=https://game-timer.virtonline.eu
traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization,button-name,button-battery-level,timestamp traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization
traefik.http.middlewares.cors-headers.headers.accesscontrolallowcredentials=true traefik.http.middlewares.cors-headers.headers.accesscontrolallowcredentials=true
traefik.http.middlewares.cors-headers.headers.accesscontrolmaxage=600 traefik.http.middlewares.cors-headers.headers.accesscontrolmaxage=600
traefik.http.middlewares.cors-headers.headers.addvaryheader=true traefik.http.middlewares.cors-headers.headers.addvaryheader=true

21
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"web-push": "^3.6.7" "web-push": "^3.6.7"
@@ -150,6 +151,18 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" "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": { "node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -601,6 +614,14 @@
"node": ">= 0.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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@@ -17,6 +17,7 @@
"author": "Your Name", "author": "Your Name",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"web-push": "^3.6.7" "web-push": "^3.6.7"

View File

@@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const webpush = require('web-push'); const webpush = require('web-push');
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 const dns = require('dns'); // Add DNS module
@@ -19,6 +20,9 @@ const basicAuthUsername = process.env.BASIC_AUTH_USERNAME;
const basicAuthPassword = process.env.BASIC_AUTH_PASSWORD; const basicAuthPassword = process.env.BASIC_AUTH_PASSWORD;
// Note: We are NOT adding specific authentication for the /subscribe endpoint in this version. // 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. // 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 // Retry configuration for DNS resolution issues
const maxRetries = parseInt(process.env.NOTIFICATION_MAX_RETRIES || 3, 10); 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 subsequentRetryDelay = parseInt(process.env.NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS || 1000, 10); // 1 second base delay for subsequent retries
@@ -178,18 +182,34 @@ loadSubscriptions();
// --- Express App Setup --- // --- Express App Setup ---
const app = express(); 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 --- // --- Body Parsing Middleware ---
app.use(express.json()); app.use(express.json());
// --- Basic Authentication Middleware --- // --- Basic Authentication Middleware ---
const authenticateBasic = (req, res, next) => { const authenticateBasic = (req, res, next) => {
// Skip authentication for OPTIONS requests (CORS preflight - Traefik might still forward them or handle them) // Skip authentication for OPTIONS requests (CORS preflight)
// It's safe to keep this check.
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
logger.debug('Auth: Skipping auth for OPTIONS request.'); return next();
// 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 // Skip authentication if username or password are not set in environment
@@ -229,7 +249,7 @@ const authenticateBasic = (req, res, next) => {
app.post('/subscribe', authenticateBasic, async (req, res) => { app.post('/subscribe', authenticateBasic, async (req, res) => {
let { button_id, subscription } = req.body; let { button_id, subscription } = req.body;
logger.debug('All headers received on /subscribe:'); logger.debug('All headers received:');
Object.keys(req.headers).forEach(headerName => { Object.keys(req.headers).forEach(headerName => {
logger.debug(` ${headerName}: ${req.headers[headerName]}`); logger.debug(` ${headerName}: ${req.headers[headerName]}`);
}); });
@@ -279,7 +299,7 @@ app.get('/webhook/:click_type', authenticateBasic, async (req, res) => {
const batteryLevel = batteryLevelHeader ? parseInt(batteryLevelHeader, 10) || batteryLevelHeader : 'N/A'; const batteryLevel = batteryLevelHeader ? parseInt(batteryLevelHeader, 10) || batteryLevelHeader : 'N/A';
// Log all headers received from Flic // Log all headers received from Flic
logger.debug('All headers received on /webhook:'); logger.debug('All headers received:');
Object.keys(req.headers).forEach(headerName => { Object.keys(req.headers).forEach(headerName => {
logger.debug(` ${headerName}: ${req.headers[headerName]}`); logger.debug(` ${headerName}: ${req.headers[headerName]}`);
}); });
@@ -342,7 +362,9 @@ const server = http.createServer(app);
server.listen(port, () => { server.listen(port, () => {
logger.info(`Flic Webhook to WebPush server listening on port ${port}`); logger.info(`Flic Webhook to WebPush server listening on port ${port}`);
logger.info('CORS: Handled by Traefik'); logger.info(`Allowed Origins: ${allowedOrigins.length > 0 ? allowedOrigins.join(', ') : '(Any)'}`);
logger.info(`Allowed Methods: ${allowedMethods.join(', ')}`);
logger.info(`Allowed Headers: ${allowedHeaders.join(', ')}`);
// Log Basic Auth status instead of Flic Secret // Log Basic Auth status instead of Flic Secret
if (basicAuthUsername && basicAuthPassword) { if (basicAuthUsername && basicAuthPassword) {
logger.info('Authentication: Basic Auth Enabled'); logger.info('Authentication: Basic Auth Enabled');
@@ -350,7 +372,6 @@ server.listen(port, () => {
logger.info('Authentication: Basic Auth Disabled (username/password not set)'); logger.info('Authentication: Basic Auth Disabled (username/password not set)');
} }
logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`); 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(`Subscriptions File: ${subscriptionsFilePath}`);
logger.info(`Push Notification Retry Config: ${maxRetries} retries, first retry: ${firstRetryDelay}ms, subsequent retries: ${subsequentRetryDelay}ms base 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(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);