commit ebc1aea8c756b36e25a1ae79a2aea5b38dadfea1 Author: Alex Rennie-Lis Date: Thu Jan 23 15:31:27 2025 +0000 first commit diff --git a/DEBIAN/control b/DEBIAN/control new file mode 100644 index 0000000..222cfff --- /dev/null +++ b/DEBIAN/control @@ -0,0 +1,10 @@ +Package: webhookd +Version: 1.0 +Section: utils +Priority: optional +Architecture: all +Depends: python3, python3-pip +Maintainer: Alex Rennie-Lis +Description: A webhook service that executes shell scripts on incoming requests + A simple webhook service configured via systemd and /etc/webhookd.conf. + diff --git a/DEBIAN/postinst b/DEBIAN/postinst new file mode 100755 index 0000000..6b49953 --- /dev/null +++ b/DEBIAN/postinst @@ -0,0 +1,28 @@ +#!/bin/bash + +# Ensure the configuration directory exists +if [ ! -f /etc/webhookd.conf ]; then + cat < /etc/webhookd.conf +# Webhook Daemon Configuration +SCRIPT_FOLDER=/webhooks/ +HOST=0.0.0.0 +PORT=8443 +CERT_FILE=/etc/ssl/certs/webhookd.crt +KEY_FILE=/etc/ssl/private/webhookd.key +EOL +fi + +# Create the system user if not exists +if ! id -u webhookd >/dev/null 2>&1; then + useradd -r -s /bin/false webhookd +fi + +# Set ownership for the script folder +mkdir -p /webhooks/ +chown -R webhookd:webhookd /webhooks/ + +# Reload systemd and enable the service +systemctl daemon-reload +systemctl enable webhookd.service +systemctl start webhookd.service + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/etc/systemd/system/webhookd.service b/etc/systemd/system/webhookd.service new file mode 100644 index 0000000..d03811a --- /dev/null +++ b/etc/systemd/system/webhookd.service @@ -0,0 +1,16 @@ +[Unit] +Description=Webhook Daemon +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /usr/local/bin/webhookd.py +WorkingDirectory=/usr/local/bin +Restart=always +User=webhookd +Group=webhookd +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target + diff --git a/usr/local/bin/webhookd.py b/usr/local/bin/webhookd.py new file mode 100644 index 0000000..fa5729a --- /dev/null +++ b/usr/local/bin/webhookd.py @@ -0,0 +1,133 @@ +#!/usr/bin/python3 + +import os +import ssl +import json +import subprocess +import shlex +from datetime import datetime +from urllib.parse import urlparse, parse_qs +from http.server import HTTPServer, BaseHTTPRequestHandler + +# Load configuration from /etc/webhookd.conf +CONFIG_FILE = '/etc/webhookd.conf' +DEFAULT_CONFIG = { + 'SCRIPT_FOLDER': '/webhooks/', + 'HOST': '0.0.0.0', + 'PORT': 8443, + 'CERT_FILE': 'server.crt', + 'KEY_FILE': 'server.key' +} + +def load_config(): + config = DEFAULT_CONFIG.copy() + if os.path.isfile(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + key, _, value = line.partition('=') + key = key.strip() + value = value.strip() + if key and value: + config[key] = value + return config + +CONFIG = load_config() + +class WebhookHandler(BaseHTTPRequestHandler): + def do_POST(self): + # Parse URL for parameters + url_parts = urlparse(self.path) + script_name = os.path.basename(url_parts.path) + query_params = parse_qs(url_parts.query) + + # Extract additional parameters from JSON body if provided + content_length = int(self.headers.get('Content-Length', 0)) + if content_length > 0: + post_data = self.rfile.read(content_length).decode('utf-8') + try: + request_data = json.loads(post_data) + query_params.update(request_data) + except json.JSONDecodeError: + self.respond(400, { + 'script': script_name, + 'timestamp': datetime.utcnow().isoformat(), + 'duration': 0, + 'exitcode': None, + 'stderr': 'Invalid JSON in request body', + 'stdout': '' + }) + return + + # Build the script path and arguments + script_path = os.path.join(CONFIG['SCRIPT_FOLDER'], script_name) + + # Check if script exists + if not os.path.isfile(script_path): + self.respond(404, { + 'script': script_name, + 'timestamp': datetime.utcnow().isoformat(), + 'duration': 0, + 'exitcode': None, + 'stderr': 'The script cannot be found', + 'stdout': '' + }) + return + + # Prepare parameters + args = [script_path] + for key, values in query_params.items(): + for value in values: + args.append(f"{shlex.quote(key)}={shlex.quote(value)}") + + # Execute the script + start_time = datetime.utcnow() + try: + result = subprocess.run( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + duration = (datetime.utcnow() - start_time).total_seconds() + self.respond(200, { + 'script': script_name, + 'timestamp': start_time.isoformat(), + 'duration': duration, + 'exitcode': result.returncode, + 'stderr': result.stderr, + 'stdout': result.stdout + }) + except Exception as e: + duration = (datetime.utcnow() - start_time).total_seconds() + self.respond(500, { + 'script': script_name, + 'timestamp': start_time.isoformat(), + 'duration': duration, + 'exitcode': None, + 'stderr': str(e), + 'stdout': '' + }) + + def respond(self, status_code, response_data): + self.send_response(status_code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(response_data).encode('utf-8')) + +# Main function to start the server +def run_server(): + httpd = HTTPServer((CONFIG['HOST'], int(CONFIG['PORT'])), WebhookHandler) + context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(certfile=CONFIG['CERT_FILE'], keyfile=CONFIG['KEY_FILE']) + + httpd.socket = context.wrap_socket(httpd.socket, server_side=True) + + print(f"Starting TLS-enabled HTTP server on {CONFIG['HOST']}:{CONFIG['PORT']}") + httpd.serve_forever() + +if __name__ == '__main__': + run_server() +