diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a6942bb --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Copy this file to .env and fill in your values. +# .env is listed in .gitignore and will NOT be committed. + +# ----- MQTT settings ----- +MQTT_SERVER=192.168.1.1 +MQTT_PORT=1883 +MQTT_PATH=homeassistant/webhook + +# Optional: MQTT broker credentials +# MQTT_USER=myuser +# MQTT_PASSWORD=mypassword + +# Optional: Enable TLS for MQTT connection (1 / true / yes) +# MQTT_TLS=false + +# ----- Webhook authentication ----- +# If set, every incoming POST must include: +# Authorization: Bearer +# Leave empty to disable authentication (not recommended in production). +# WEBHOOK_TOKEN=change-me-to-a-strong-random-secret + +# ----- Timezone ----- +TZ=Europe/Berlin + +# ----- Traefik labels (adapt to your domain) ----- +WEBHOOK_HOST=webhook.example.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Dockerfile b/Dockerfile index e1fc552..f943205 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM tiangolo/meinheld-gunicorn-flask:latest +FROM python:3.12-slim -RUN apt-get update && apt-get install -y \ - python3-pip +WORKDIR /app -RUN /usr/local/bin/python -m pip install --upgrade pip -RUN pip3 install --upgrade Flask -RUN pip3 install paho-mqtt +COPY requirements.txt . +RUN python -m pip install --no-cache-dir -r requirements.txt COPY ./app /app + +CMD ["gunicorn", "--bind", "0.0.0.0:80", "main:app"] diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..951984c Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/main.py b/app/main.py index fcab247..3549f32 100644 --- a/app/main.py +++ b/app/main.py @@ -4,22 +4,19 @@ import paho.mqtt.client as mqtt import json import datetime - import os -#$env:MQTT_SERVER='192.168.176.6' +import sys + #-------------Output Logger # create logger logger = logging.getLogger("Webhook2MQTT") -#logger.setLevel(logging.INFO) logger.setLevel(logging.INFO) # create console handler with a higher log level ch = logging.StreamHandler() -#ch.setLevel(logging.INFO) ch.setLevel(logging.INFO) # create formatter and add it to the handlers formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') -#formatter = logging.Formatter('%(levelname)s - %(message)s') ch.setFormatter(formatter) # add the handlers to the logger logger.addHandler(ch) @@ -29,7 +26,7 @@ mqtt_server = os.environ['MQTT_SERVER'] else: logger.error("Please set environment-Variable for MQTT_SERVER") - os.exit() + sys.exit(1) logger.info("set MQTT_SERVER to {}".format(mqtt_server)) if 'MQTT_PORT' in os.environ: @@ -44,25 +41,54 @@ mqtt_path = 'webhook' logger.info("set mqtt-path to '{}'".format(mqtt_path)) +mqtt_user = os.environ.get('MQTT_USER') +mqtt_password = os.environ.get('MQTT_PASSWORD') +mqtt_tls = os.environ.get('MQTT_TLS', '').lower() in ('1', 'true', 'yes') +if mqtt_user: + logger.info("MQTT authentication enabled") +if mqtt_tls: + logger.info("MQTT TLS enabled") + +webhook_token = os.environ.get('WEBHOOK_TOKEN') +if webhook_token: + logger.info("Webhook token authentication enabled") +else: + logger.warning("WEBHOOK_TOKEN is not set — webhook endpoint is unauthenticated") app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1 MB limit + def workit(params): logger.info("workit:") - logger.info(params) - params['timestamp'] = datetime.datetime.now().isoformat() - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) - client.connect(mqtt_server, mqtt_port, 60) - client.publish(mqtt_path, json.dumps(params),qos=0,retain=True) - client.disconnect() + logger.info(params) + try: + params['timestamp'] = datetime.datetime.now().isoformat() + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + if mqtt_tls: + client.tls_set() + client.connect(mqtt_server, mqtt_port, 60) + client.publish(mqtt_path, json.dumps(params), qos=0, retain=True) + client.disconnect() + except Exception as e: + logger.error("Failed to publish MQTT message: {}".format(e)) + @app.route('/', methods=['POST']) def respond(): logger.info(request) - myparams=request.get_json() + if webhook_token: + auth_header = request.headers.get('Authorization', '') + if auth_header != 'Bearer {}'.format(webhook_token): + logger.warning("Unauthorized request from {}".format(request.remote_addr)) + return Response(status=401) + myparams = request.get_json() + if myparams is None: + logger.warning("Received non-JSON or empty request body") + return Response(status=400) x = threading.Thread(target=workit, args=(myparams,)) x.start() return Response(status=200) -#app.run (host = "localhost", port = 5050) - diff --git a/docker-compose.yml b/docker-compose.yml index 7a55654..1e65a3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.6" services: webhook2mqtt: build: . @@ -11,24 +10,27 @@ services: - "/etc/localtime:/etc/localtime:ro" - "/etc/timezone:/etc/timezone:ro" environment: - TZ: "Europe/Berlin" - MQTT_SERVER: 'home.jru.me' - MQTT_PORT: 1883 - MQTT_PATH: 'homeassistant/webhook' + TZ: ${TZ:-Europe/Berlin} + MQTT_SERVER: ${MQTT_SERVER} + MQTT_PORT: ${MQTT_PORT:-1883} + MQTT_PATH: ${MQTT_PATH:-webhook} + # MQTT_USER: ${MQTT_USER} + # MQTT_PASSWORD: ${MQTT_PASSWORD} + # MQTT_TLS: ${MQTT_TLS:-false} + # WEBHOOK_TOKEN: ${WEBHOOK_TOKEN} # example config for https with traefik # networks: # - traefik_proxy labels: - "traefik.enable=true" - "traefik.http.routers.webhook2mqtt_router_http.entrypoints=web" - - "traefik.http.routers.webhook2mqtt_router_http.rule=Host(`netatmo.jru.me`)" + - "traefik.http.routers.webhook2mqtt_router_http.rule=Host(`${WEBHOOK_HOST}`)" # - "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https" # - "traefik.http.routers.webhook2mqtt_router_http.middlewares=https_redirect" - "traefik.http.routers.webhook2mqtt_router.entrypoints=websecure" - - "traefik.http.routers.webhook2mqtt_router.rule=Host(`netatmo.jru.me`)" + - "traefik.http.routers.webhook2mqtt_router.rule=Host(`${WEBHOOK_HOST}`)" - "traefik.http.routers.webhook2mqtt_router.tls=true" - "traefik.http.routers.webhook2mqtt_router.tls.certresolver=my_certresolver" - restart: always # log to syslog | optional logging: driver: syslog diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71c015d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.1.3 +paho-mqtt==2.1.0 +gunicorn==26.0.0