import { createHmac , timingSafeEqual } from "crypto" ; import { createServer , IncomingMessage , Server , ServerResponse } from "http" ; import { setInterval } from "timers" ; import full from "../full" ; import { Fetcher , stringOfStream } from "../util/io" ; import { joinLogWithErrors , LoggerWithErrors , loggerWithErrors , LogWithErrors } from "../util/logging" ; import { currentTimeStamp , parseJson } from "../util/util" ; import { Options } from "./common" ; import RollingLogs from "./rolling-logs" ; import { sourceBranch } from "./settings" ; export default async function webhookServer ( key , githubAccessToken , dry , fetcher , options ) { const fullOne = updateOneAtATime ( async ( log , timestamp ) => { log . info ( "" ) ; log . info ( "" ) ; log . info ( `# ${ timestamp }` ) ; log . info ( "" ) ; log . info ( "Starting full..." ) ; await full ( dry , timestamp , githubAccessToken , fetcher , options , log ) ; } ) ; setInterval ( ( log , timestamp ) => { const result = fullOne ( log , timestamp ) ; if ( ! result ) { return ; } result . catch ( e => { log . info ( e . toString ( ) ) ; console . error ( e ) ; } ) ; } , 2000000 , loggerWithErrors ( ) [ 0 ] , currentTimeStamp ( ) ) ; return listenToGithub ( key , fullOne ) ; } function writeLog ( rollingLogs , logs ) { return rollingLogs . write ( joinLogWithErrors ( logs ) ) ; } param onUpdate function listenToGithub ( key , onUpdate ) { console . log ( "Before starting server" ) ; const rollingLogs = RollingLogs . create ( "webhook-logs.md" , 1000 ) ; const server = createServer ( ( req , resp ) => { switch ( req . method ) { case "POST" : receiveUpdate ( req , resp ) ; break ; default : } } ) ; return server ; function receiveUpdate ( req , resp ) { const [ log , logResult ] = loggerWithErrors ( ) ; const timeStamp = currentTimeStamp ( ) ; try { log . info ( "Before starting work" ) ; work ( ) . then ( ( ) => rollingLogs . then ( logs => writeLog ( logs , logResult ( ) ) ) ) . catch ( onError ) ; } catch ( error ) { rollingLogs . then ( logs => writeLog ( logs , logResult ( ) ) ) . then ( onError ) . catch ( onError ) ; } function onError ( ) { server . close ( ) ; } async function work ( ) { const data = await stringOfStream ( req , "Request to webhook" ) ; if ( ! checkSignature ( key , data , req . headers , log ) ) { return ; } log . info ( `Message from github: ${ data . slice ( 0 , 200 ) }...` ) ; const expectedRef = `refs/heads/${ sourceBranch }` ; const actualRef = ( parseJson ( data ) as { readonly ref : string ; } ) . ref ; if ( actualRef === expectedRef ) { respond ( "Thanks for the update! Running full." ) ; await onUpdate ( log , timeStamp ) ; } else { const text = `Ignoring push to ${ actualRef }, expected ${ expectedRef }.` ; respond ( text ) ; log . info ( text ) ; } } function respond ( text ) { resp . write ( text ) ; resp . end ( ) ; } } } function updateOneAtATime ( doOnce ) { let working = false ; return ( log , timeStamp ) => { if ( working ) { log . info ( "Not starting update, because already performing one." ) ; return undefined ; } return work ( ) ; async function work ( ) { log . info ( "Starting update" ) ; working = true ; try { await doOnce ( log , timeStamp ) ; } catch ( e ) { log . info ( e . toString ( ) ) ; } finally { working = false ; } } } ; } function checkSignature ( key , data , headers , log ) { const signature = headers [ "x-hub-signature" ] ; const expected = expectedSignature ( key , data ) ; if ( typeof signature === "string" && stringEqualsConstantTime ( signature , expected ) ) { return true ; } log . error ( `Invalid request: expected ${ expected }, got ${ signature }` ) ; log . error ( `Headers are: ${ JSON . stringify ( headers , undefined , 4 ) }` ) ; log . error ( `Data is: ${ data }` ) ; log . error ( "" ) ; return false ; } function stringEqualsConstantTime ( actual , expected ) { const actualBuffer = Buffer . alloc ( expected . length ) ; actualBuffer . write ( actual ) ; return timingSafeEqual ( actualBuffer , Buffer . from ( expected ) ) ; } export function expectedSignature ( key , data ) { const hmac = createHmac ( "sha1" , key ) ; hmac . write ( data ) ; const digest = hmac . digest ( "hex" ) ; return `sha1=${ digest }` ; }