@@ -285,7 +285,7 @@ const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
285285
286286const openaiBridgeHandler = createOpenaiBridgeHttpHandler ( {
287287 settingsFile : OPENAI_BRIDGE_SETTINGS_FILE ,
288- expectedToken : 'codexmate ',
288+ expectedToken : typeof process . env . CODEXMATE_HTTP_TOKEN === 'string' ? process . env . CODEXMATE_HTTP_TOKEN . trim ( ) : ' ',
289289 maxBodySize : MAX_API_BODY_SIZE ,
290290 httpAgent : HTTP_KEEP_ALIVE_AGENT ,
291291 httpsAgent : HTTPS_KEEP_ALIVE_AGENT
@@ -7964,7 +7964,7 @@ function writeJsonResponse(res, statusCode, payload) {
79647964function readJsonRequestBody ( req , res , options = { } ) {
79657965 const maxBytes = Number . isFinite ( options . maxBytes ) ? Math . max ( 1024 , Math . floor ( options . maxBytes ) ) : MAX_API_BODY_SIZE ;
79667966 return new Promise ( ( resolve ) => {
7967- let body = '' ;
7967+ const chunks = [ ] ;
79687968 let bodySize = 0 ;
79697969 let bodyTooLarge = false ;
79707970 req . on ( 'data' , ( chunk ) => {
@@ -7979,12 +7979,14 @@ function readJsonRequestBody(req, res, options = {}) {
79797979 resolve ( { ok : false , error : 'payload-too-large' } ) ;
79807980 return ;
79817981 }
7982- body += chunk ;
7982+ chunks . push ( chunk ) ;
79837983 } ) ;
79847984 req . on ( 'end' , ( ) => {
79857985 if ( bodyTooLarge ) return ;
7986+ const rawBuffer = chunks . length ? Buffer . concat ( chunks ) : Buffer . alloc ( 0 ) ;
7987+ const rawText = rawBuffer . length ? rawBuffer . toString ( 'utf-8' ) : '' ;
79867988 try {
7987- resolve ( { ok : true , body : JSON . parse ( body || '{}' ) } ) ;
7989+ resolve ( { ok : true , body : JSON . parse ( rawText || '{}' ) , rawText , rawBuffer } ) ;
79887990 } catch ( error ) {
79897991 resolve ( { ok : false , error : error && error . message ? error . message : 'invalid json' } ) ;
79907992 }
@@ -8064,6 +8066,25 @@ function rememberWebhookDeliveryId(value, ttlMs = 10 * 60 * 1000) {
80648066 return { ok : true , seen : false } ;
80658067}
80668068
8069+ function safeTimingEqual ( a , b ) {
8070+ try {
8071+ const ba = Buffer . isBuffer ( a ) ? a : Buffer . from ( String ( a || '' ) , 'utf-8' ) ;
8072+ const bb = Buffer . isBuffer ( b ) ? b : Buffer . from ( String ( b || '' ) , 'utf-8' ) ;
8073+ if ( ba . length !== bb . length ) return false ;
8074+ return crypto . timingSafeEqual ( ba , bb ) ;
8075+ } catch ( _ ) {
8076+ return false ;
8077+ }
8078+ }
8079+
8080+ function verifyGithubWebhookSignature ( secret , signatureHeader , rawBuffer ) {
8081+ const key = typeof secret === 'string' ? secret : '' ;
8082+ const signature = typeof signatureHeader === 'string' ? signatureHeader . trim ( ) : '' ;
8083+ if ( ! key || ! signature || ! signature . startsWith ( 'sha256=' ) ) return false ;
8084+ const expected = 'sha256=' + crypto . createHmac ( 'sha256' , key ) . update ( rawBuffer || Buffer . alloc ( 0 ) ) . digest ( 'hex' ) ;
8085+ return safeTimingEqual ( signature , expected ) ;
8086+ }
8087+
80678088async function handleAutomationHook ( req , res , source ) {
80688089 const method = ( req . method || 'GET' ) . toUpperCase ( ) ;
80698090 if ( method !== 'POST' ) {
@@ -8085,6 +8106,42 @@ async function handleAutomationHook(req, res, source) {
80858106 }
80868107 return ;
80878108 }
8109+ const remoteAddr = req && req . socket ? req . socket . remoteAddress : '' ;
8110+ const isLoopback = ! remoteAddr || isLoopbackRemoteAddress ( remoteAddr ) ;
8111+ const normalizedSource = typeof source === 'string' ? source . trim ( ) . toLowerCase ( ) : '' ;
8112+ if ( normalizedSource === 'github' ) {
8113+ const secret = typeof process . env . CODEXMATE_GITHUB_WEBHOOK_SECRET === 'string'
8114+ ? process . env . CODEXMATE_GITHUB_WEBHOOK_SECRET
8115+ : '' ;
8116+ if ( ! secret && ! isLoopback ) {
8117+ writeJsonResponse ( res , 403 , { error : 'Remote GitHub webhook is disabled (set CODEXMATE_GITHUB_WEBHOOK_SECRET)' } ) ;
8118+ return ;
8119+ }
8120+ if ( secret ) {
8121+ const signature = ( req . headers || { } ) [ 'x-hub-signature-256' ] ;
8122+ if ( ! verifyGithubWebhookSignature ( secret , signature , parsedBody . rawBuffer ) ) {
8123+ writeJsonResponse ( res , 401 , { error : 'Invalid webhook signature' } ) ;
8124+ return ;
8125+ }
8126+ }
8127+ } else if ( normalizedSource === 'gitlab' ) {
8128+ const secret = typeof process . env . CODEXMATE_GITLAB_WEBHOOK_SECRET === 'string'
8129+ ? process . env . CODEXMATE_GITLAB_WEBHOOK_SECRET . trim ( )
8130+ : '' ;
8131+ if ( ! secret && ! isLoopback ) {
8132+ writeJsonResponse ( res , 403 , { error : 'Remote GitLab webhook is disabled (set CODEXMATE_GITLAB_WEBHOOK_SECRET)' } ) ;
8133+ return ;
8134+ }
8135+ if ( secret ) {
8136+ const tokenHeader = typeof ( req . headers || { } ) [ 'x-gitlab-token' ] === 'string'
8137+ ? String ( req . headers [ 'x-gitlab-token' ] ) . trim ( )
8138+ : '' ;
8139+ if ( ! tokenHeader || tokenHeader !== secret ) {
8140+ writeJsonResponse ( res , 401 , { error : 'Invalid webhook token' } ) ;
8141+ return ;
8142+ }
8143+ }
8144+ }
80888145 const payload = parsedBody . body && typeof parsedBody . body === 'object' ? parsedBody . body : { } ;
80898146 const eventKey = buildAutomationEventKey ( source , req . headers || { } , payload ) ;
80908147 if ( ! eventKey ) {
0 commit comments