/** * HTTP server for receiving Gogs webhooks */ import http from 'http'; import logger from './logger.js'; import { WebhookHandler } from './webhookHandler.js'; export class WebhookServer { constructor(options = {}) { this.host = options.host || '0.0.0.0'; this.port = options.port || 3000; this.path = options.path || '/webhook'; this.secret = options.secret || null; this.webhookHandler = new WebhookHandler(); this.server = null; } /** * Parse the request body */ _parseBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { resolve(body ? JSON.parse(body) : {}); } catch (error) { reject(new Error('Invalid JSON payload')); } }); req.on('error', reject); }); } /** * Verify webhook signature if secret is configured */ _verifySignature(signature, payload) { if (!this.secret) { return true; } // Gogs uses HMAC-SHA256 for signatures // Format: X-Gogs-Signature: // Implementation can be added when secret verification is needed logger.info('Signature verification skipped (not implemented)'); return true; } /** * Handle incoming HTTP requests */ async _handleRequest(req, res) { const { method, url } = req; // Health check endpoint if (url === '/health' && method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() })); return; } // Webhook endpoint if (url === this.path && method === 'POST') { try { const payload = await this._parseBody(req); const eventType = req.headers['x-gogs-event'] || 'unknown'; const signature = req.headers['x-gogs-signature']; const delivery = req.headers['x-gogs-delivery']; // Log basic request info logger.info('Received webhook', { event: eventType, delivery: delivery, hasSignature: !!signature }); // Verify signature if configured if (!this._verifySignature(signature, payload)) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid signature' })); return; } // Process webhook await this.webhookHandler.handle(eventType, payload, req.headers); // Send success response res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'received' })); } catch (error) { logger.error('Error processing webhook', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } return; } // 404 for other routes res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); } /** * Start the server */ start() { this.server = http.createServer((req, res) => this._handleRequest(req, res)); this.server.listen(this.port, this.host, () => { logger.info(`Webhook server listening on ${this.host}:${this.port}`); logger.info(`Webhook endpoint: http://${this.host}:${this.port}${this.path}`); logger.info(`Health check: http://${this.host}:${this.port}/health`); }); return this; } /** * Stop the server */ stop() { return new Promise((resolve) => { if (this.server) { this.server.close(() => { logger.info('Server stopped'); resolve(); }); } else { resolve(); } }); } /** * Get the webhook handler instance for registering callbacks */ getHandler() { return this.webhookHandler; } }