|
@@ -2,6 +2,7 @@
|
|
|
* HTTP server for receiving Gogs webhooks
|
|
* HTTP server for receiving Gogs webhooks
|
|
|
*/
|
|
*/
|
|
|
import http from 'http';
|
|
import http from 'http';
|
|
|
|
|
+import crypto from 'crypto';
|
|
|
import logger from './logger.js';
|
|
import logger from './logger.js';
|
|
|
import { WebhookHandler } from './webhookHandler.js';
|
|
import { WebhookHandler } from './webhookHandler.js';
|
|
|
|
|
|
|
@@ -17,6 +18,7 @@ export class WebhookServer {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Parse the request body
|
|
* Parse the request body
|
|
|
|
|
+ * Returns both the raw body string and parsed JSON
|
|
|
*/
|
|
*/
|
|
|
_parseBody(req) {
|
|
_parseBody(req) {
|
|
|
return new Promise((resolve, reject) => {
|
|
return new Promise((resolve, reject) => {
|
|
@@ -26,7 +28,8 @@ export class WebhookServer {
|
|
|
});
|
|
});
|
|
|
req.on('end', () => {
|
|
req.on('end', () => {
|
|
|
try {
|
|
try {
|
|
|
- resolve(body ? JSON.parse(body) : {});
|
|
|
|
|
|
|
+ const parsed = body ? JSON.parse(body) : {};
|
|
|
|
|
+ resolve({ raw: body, parsed });
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
reject(new Error('Invalid JSON payload'));
|
|
reject(new Error('Invalid JSON payload'));
|
|
|
}
|
|
}
|
|
@@ -37,16 +40,45 @@ export class WebhookServer {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Verify webhook signature if secret is configured
|
|
* Verify webhook signature if secret is configured
|
|
|
|
|
+ * @param {string} signature - The signature from X-Gogs-Signature header
|
|
|
|
|
+ * @param {string} rawPayload - The raw request body as string
|
|
|
|
|
+ * @returns {boolean} - True if signature is valid or no secret is configured
|
|
|
*/
|
|
*/
|
|
|
- _verifySignature(signature, payload) {
|
|
|
|
|
|
|
+ _verifySignature(signature, rawPayload) {
|
|
|
|
|
+ // If no secret is configured, skip verification
|
|
|
if (!this.secret) {
|
|
if (!this.secret) {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
- // Gogs uses HMAC-SHA256 for signatures
|
|
|
|
|
- // Format: X-Gogs-Signature: <hmac-sha256-hex>
|
|
|
|
|
- // Implementation can be added when secret verification is needed
|
|
|
|
|
- logger.info('Signature verification skipped (not implemented)');
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Signature is required when secret is configured
|
|
|
|
|
+ if (!signature) {
|
|
|
|
|
+ logger.error('Signature verification failed: X-Gogs-Signature header missing');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Compute HMAC-SHA256 of the raw payload using the secret
|
|
|
|
|
+ const hmac = crypto.createHmac('sha256', this.secret);
|
|
|
|
|
+ hmac.update(rawPayload, 'utf8');
|
|
|
|
|
+ const computedSignature = hmac.digest('hex');
|
|
|
|
|
+
|
|
|
|
|
+ // Compare signatures using timing-safe comparison
|
|
|
|
|
+ const isValid = crypto.timingSafeEqual(
|
|
|
|
|
+ Buffer.from(signature),
|
|
|
|
|
+ Buffer.from(computedSignature)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (isValid) {
|
|
|
|
|
+ logger.info('Signature verification successful');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logger.error('Signature verification failed: signature mismatch');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return isValid;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Signature verification failed:', error);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -65,7 +97,7 @@ export class WebhookServer {
|
|
|
// Webhook endpoint
|
|
// Webhook endpoint
|
|
|
if (url === this.path && method === 'POST') {
|
|
if (url === this.path && method === 'POST') {
|
|
|
try {
|
|
try {
|
|
|
- const payload = await this._parseBody(req);
|
|
|
|
|
|
|
+ const { raw, parsed } = await this._parseBody(req);
|
|
|
const eventType = req.headers['x-gogs-event'] || 'unknown';
|
|
const eventType = req.headers['x-gogs-event'] || 'unknown';
|
|
|
const signature = req.headers['x-gogs-signature'];
|
|
const signature = req.headers['x-gogs-signature'];
|
|
|
const delivery = req.headers['x-gogs-delivery'];
|
|
const delivery = req.headers['x-gogs-delivery'];
|
|
@@ -74,18 +106,19 @@ export class WebhookServer {
|
|
|
logger.info('Received webhook', {
|
|
logger.info('Received webhook', {
|
|
|
event: eventType,
|
|
event: eventType,
|
|
|
delivery: delivery,
|
|
delivery: delivery,
|
|
|
- hasSignature: !!signature
|
|
|
|
|
|
|
+ hasSignature: !!signature,
|
|
|
|
|
+ hasSecret: !!this.secret
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Verify signature if configured
|
|
// Verify signature if configured
|
|
|
- if (!this._verifySignature(signature, payload)) {
|
|
|
|
|
|
|
+ if (!this._verifySignature(signature, raw)) {
|
|
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
|
res.end(JSON.stringify({ error: 'Invalid signature' }));
|
|
res.end(JSON.stringify({ error: 'Invalid signature' }));
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Process webhook
|
|
// Process webhook
|
|
|
- await this.webhookHandler.handle(eventType, payload, req.headers);
|
|
|
|
|
|
|
+ await this.webhookHandler.handle(eventType, parsed, req.headers);
|
|
|
|
|
|
|
|
// Send success response
|
|
// Send success response
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|