From fa73c9ea6dc4f136e1dabac85ed02d1f02b11427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 6 Mar 2025 15:07:51 +0100 Subject: [PATCH] first commit --- .gitignore | 4 ++ README.md | 0 app.py | 135 +++++++++++++++++++++++++++++++++++++++++++ hosts_daemon.service | 14 +++++ requirements.txt | 8 +++ ssl.txt | 5 ++ 6 files changed, 166 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 hosts_daemon.service create mode 100644 requirements.txt create mode 100644 ssl.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1931769 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +data/ +instance/ +venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app.py new file mode 100644 index 0000000..22b5b4a --- /dev/null +++ b/app.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +import os +import time +import logging +import difflib +from flask import Flask, request, jsonify, abort +from flask_sslify import SSLify +from datetime import datetime, timezone, timedelta +from croniter import croniter +import psutil + +app = Flask(__name__) +sslify = SSLify(app) + +# Konfiguracja tokenu autoryzacyjnego (przyjmujemy pre-shared secret) +API_TOKEN = os.environ.get("HOSTS_DAEMON_API_TOKEN", "changeme") + +# Konfiguracja logowania – logi zapisywane do pliku /opt/hosts_daemon/logs/daemon.log +LOG_DIR = "/opt/hosts_daemon/logs" +os.makedirs(LOG_DIR, exist_ok=True) +LOG_FILE = os.path.join(LOG_DIR, "daemon.log") +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler(LOG_FILE), + logging.StreamHandler() + ] +) +logger = logging.getLogger("hosts_daemon") + +# Globalne metryki +metrics = { + "total_requests": 0, + "total_time": 0.0, + "endpoints": {}, + "hosts_get": 0, + "hosts_post": 0, +} + +# Funkcja sprawdzająca autoryzację +def require_auth(): + token = request.headers.get("Authorization") + if token != API_TOKEN: + abort(401, description="Unauthorized") + +# Logowanie i zbieranie metryk – logujemy czas rozpoczęcia, adres IP, endpoint i metodę +@app.before_request +def before_request_logging(): + request.start_time = time.time() + client_ip = request.remote_addr + endpoint = request.path + logger.info(f"Request from {client_ip} to {endpoint} [{request.method}], Auth: {request.headers.get('Authorization')}") + metrics["total_requests"] += 1 + if endpoint not in metrics["endpoints"]: + metrics["endpoints"][endpoint] = {"count": 0, "total_time": 0.0} + metrics["endpoints"][endpoint]["count"] += 1 + +@app.after_request +def after_request_logging(response): + elapsed = time.time() - request.start_time + metrics["total_time"] += elapsed + endpoint = request.path + if endpoint in metrics["endpoints"]: + metrics["endpoints"][endpoint]["total_time"] += elapsed + logger.info(f"Completed {endpoint} in {elapsed:.3f} sec with status {response.status_code}") + return response + +# Endpoint GET /hosts – pobiera zawartość /etc/hosts +@app.route('/hosts', methods=['GET']) +def get_hosts(): + require_auth() + metrics["hosts_get"] += 1 + try: + with open('/etc/hosts', 'r') as f: + content = f.read() + logger.info(f"/hosts GET successful from {request.remote_addr}") + return jsonify({"hosts": content}) + except Exception as e: + logger.error(f"/hosts GET error: {str(e)}") + return jsonify({"error": str(e)}), 500 + +# Endpoint POST /hosts – aktualizuje plik /etc/hosts +@app.route('/hosts', methods=['POST']) +def update_hosts(): + require_auth() + metrics["hosts_post"] += 1 + data = request.get_json() + if not data or "hosts" not in data: + logger.warning(f"/hosts POST: missing 'hosts' key from {request.remote_addr}") + return jsonify({"error": "Invalid request, missing 'hosts' key"}), 400 + new_content = data["hosts"] + try: + with open('/etc/hosts', 'w') as f: + f.write(new_content) + logger.info(f"/hosts POST updated by {request.remote_addr}") + return jsonify({"message": "File updated successfully"}) + except Exception as e: + logger.error(f"/hosts POST error: {str(e)}") + return jsonify({"error": str(e)}), 500 + +# Endpoint /health – zwraca status usługi +@app.route('/health', methods=['GET']) +def health(): + require_auth() + uptime = time.time() - psutil.boot_time() + now = datetime.now(timezone.utc).isoformat() + logger.info(f"/health check from {request.remote_addr}") + return jsonify({"status": "ok", "time": now, "uptime": uptime}) + +# Endpoint /metrics – zwraca metryki +@app.route('/metrics', methods=['GET']) +def metrics_endpoint(): + require_auth() + avg_time = metrics["total_time"] / metrics["total_requests"] if metrics["total_requests"] > 0 else 0.0 + ep_data = {} + for ep, data in metrics["endpoints"].items(): + ep_avg = data["total_time"] / data["count"] if data["count"] > 0 else 0.0 + ep_data[ep] = {"count": data["count"], "avg_time": ep_avg} + response_data = { + "total_requests": metrics["total_requests"], + "avg_response_time": avg_time, + "endpoints": ep_data, + "hosts_get": metrics.get("hosts_get", 0), + "hosts_post": metrics.get("hosts_post", 0) + } + logger.info(f"/metrics accessed by {request.remote_addr}") + return jsonify(response_data) + +if __name__ == '__main__': + app.run( + host='0.0.0.0', + port=8000, + ssl_context=('/opt/hosts_daemon/ssl/hosts_daemon.crt', '/opt/hosts_daemon/ssl/hosts_daemon.key') + ) diff --git a/hosts_daemon.service b/hosts_daemon.service new file mode 100644 index 0000000..007075e --- /dev/null +++ b/hosts_daemon.service @@ -0,0 +1,14 @@ +[Unit] +Description=Hosts Daemon Service +After=network.target + +[Service] +User=hostsdaemon +Group=hostsdaemon +WorkingDirectory=/opt/hosts_daemon +ExecStart=/opt/hosts_daemon/venv/bin/gunicorn --bind 0.0.0.0:8000 --certfile=/opt/hosts_daemon/ssl/hosts_daemon.crt --keyfile=/opt/hosts_daemon/ssl/hosts_daemon.key app:app +Restart=always +Environment="PATH=/opt/hosts_daemon/venv/bin" + +[Install] +WantedBy=multi-user.target diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0330247 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask +Flask-SSLify +Flask-SQLAlchemy +paramiko +croniter +tzlocal +gunicorn +psutil diff --git a/ssl.txt b/ssl.txt new file mode 100644 index 0000000..590e1ac --- /dev/null +++ b/ssl.txt @@ -0,0 +1,5 @@ +sudo mkdir -p /opt/hosts_daemon/ssl +sudo openssl req -x509 -nodes -days 3650-newkey rsa:2048 \ + -keyout /opt/hosts_daemon/ssl/hosts_daemon.key \ + -out /opt/hosts_daemon/ssl/hosts_daemon.crt \ + -subj "/C=PL/ST=WLKP/L=POZ/O=linuxiarz.pl/OU=hostsDaemon/CN=0.0.0.0"