2024-01-02 15:52:43 +00:00
2023-03-14 10:50:46 +00:00
/* eslint-disable camelcase */
2023-05-18 11:38:59 +01:00
import { BridgeConfig } from "./config/Config" ;
2021-12-21 16:52:12 +00:00
import { Router , default as express , Request , Response } from "express" ;
2019-08-02 19:12:03 +01:00
import { EventEmitter } from "events" ;
2021-11-29 16:34:13 +00:00
import { MessageQueue , createMessageQueue } from "./MessageQueue" ;
2023-03-14 10:50:46 +00:00
import { ApiError , ErrCode , Logger } from "matrix-appservice-bridge" ;
2019-08-08 15:36:44 +01:00
import qs from "querystring" ;
2020-02-18 14:42:57 +00:00
import axios from "axios" ;
2022-04-07 16:49:03 +01:00
import { IGitLabWebhookEvent , IGitLabWebhookIssueStateEvent , IGitLabWebhookMREvent , IGitLabWebhookReleaseEvent } from "./Gitlab/WebhookTypes" ;
2024-11-28 15:04:01 +00:00
import { EmitterWebhookEvent , Webhooks as OctokitWebhooks } from "@octokit/webhooks"
2023-05-18 11:38:59 +01:00
import { IJiraWebhookEvent } from "./jira/WebhookTypes" ;
import { JiraWebhooksRouter } from "./jira/Router" ;
2021-11-29 16:34:13 +00:00
import { OAuthRequest } from "./WebhookTypes" ;
2023-05-18 11:38:59 +01:00
import { GitHubOAuthTokenResponse } from "./github/Types" ;
2021-12-16 15:05:03 +00:00
import Metrics from "./Metrics" ;
2022-01-06 17:13:58 +00:00
import { FigmaWebhooksRouter } from "./figma/router" ;
2022-03-07 23:57:06 +00:00
import { GenericWebhooksRouter } from "./generic/Router" ;
2023-05-18 11:38:59 +01:00
import { GithubInstance } from "./github/GithubInstance" ;
2022-08-12 10:59:03 +01:00
import QuickLRU from "@alloc/quick-lru" ;
2024-11-28 15:04:01 +00:00
import type { WebhookEventName } from "@octokit/webhooks-types" ;
2022-03-04 14:34:44 +00:00
2022-10-05 14:49:53 +01:00
const log = new Logger ( "Webhooks" ) ;
2021-05-02 17:09:17 +01:00
2020-02-18 17:16:21 +00:00
export interface NotificationsEnableEvent {
2020-11-22 21:10:27 +00:00
userId : string ;
roomId : string ;
2022-07-11 18:08:09 +01:00
since? : number ;
2020-02-18 17:16:21 +00:00
token : string ;
2020-11-22 21:10:27 +00:00
filterParticipating : boolean ;
type : "github" | "gitlab" ;
instanceUrl? : string ;
2020-02-18 17:16:21 +00:00
}
export interface NotificationsDisableEvent {
2020-11-22 21:10:27 +00:00
userId : string ;
type : "github" | "gitlab" ;
instanceUrl? : string ;
2020-02-18 17:16:21 +00:00
}
2023-03-21 10:46:08 +00:00
export interface OAuthPageParams {
service? : string ;
result? : string ;
'oauth-kind' ? : 'account' | 'organisation' ;
'error' ? : string ;
'errcode' ? : ErrCode ;
}
2024-01-02 15:52:43 +00:00
interface GitHubRequestData {
payload : string ;
signature : string ;
}
interface WebhooksExpressRequest extends Request {
github? : GitHubRequestData ;
}
2021-05-02 17:09:17 +01:00
export class Webhooks extends EventEmitter {
2022-03-04 14:34:44 +00:00
2021-12-21 16:52:12 +00:00
public readonly expressRouter = Router ( ) ;
2022-08-12 10:59:03 +01:00
private readonly queue : MessageQueue ;
private readonly ghWebhooks? : OctokitWebhooks ;
private readonly handledGuids = new QuickLRU < string , void > ( { maxAge : 5000 , maxSize : 100 } ) ;
2019-08-02 19:12:03 +01:00
constructor ( private config : BridgeConfig ) {
super ( ) ;
2021-12-21 16:52:12 +00:00
this . expressRouter . use ( ( req , _res , next ) = > {
2021-12-16 15:05:03 +00:00
Metrics . webhooksHttpRequest . inc ( { path : req.path , method : req.method } ) ;
next ( ) ;
} ) ;
2021-04-25 19:40:02 +01:00
if ( this . config . github ? . webhook . secret ) {
this . ghWebhooks = new OctokitWebhooks ( {
secret : config.github?.webhook.secret as string ,
} ) ;
2021-05-02 17:40:48 +01:00
this . ghWebhooks . onAny ( e = > this . onGitHubPayload ( e ) ) ;
2021-04-25 19:40:02 +01:00
}
2021-12-01 17:22:48 +00:00
// TODO: Move these
2021-12-21 16:52:12 +00:00
this . expressRouter . get ( "/oauth" , this . onGitHubGetOauth . bind ( this ) ) ;
2022-05-06 14:58:39 +01:00
this . queue = createMessageQueue ( config . queue ) ;
2022-01-06 17:13:58 +00:00
if ( this . config . jira ) {
2022-03-04 14:34:44 +00:00
this . expressRouter . use ( "/jira" , new JiraWebhooksRouter ( this . queue ) . getRouter ( ) ) ;
2022-01-06 17:13:58 +00:00
}
if ( this . config . figma ) {
this . expressRouter . use ( '/figma' , new FigmaWebhooksRouter ( this . config . figma , this . queue ) . getRouter ( ) ) ;
}
2022-03-07 23:57:06 +00:00
if ( this . config . generic ) {
2022-07-07 22:52:59 +01:00
this . expressRouter . use ( '/webhook' , new GenericWebhooksRouter ( this . queue , false , this . config . generic . enableHttpGet ) . getRouter ( ) ) ;
2022-03-07 23:57:06 +00:00
// TODO: Remove old deprecated endpoint
2022-07-07 22:52:59 +01:00
this . expressRouter . use ( new GenericWebhooksRouter ( this . queue , true , this . config . generic . enableHttpGet ) . getRouter ( ) ) ;
2022-03-07 23:57:06 +00:00
}
2021-12-21 16:52:12 +00:00
this . expressRouter . use ( express . json ( {
2019-08-02 19:12:03 +01:00
verify : this.verifyRequest.bind ( this ) ,
2023-01-04 12:04:58 +00:00
limit : '10mb' ,
2019-08-02 19:12:03 +01:00
} ) ) ;
2021-12-21 16:52:12 +00:00
this . expressRouter . post ( "/" , this . onPayload . bind ( this ) ) ;
2019-08-02 19:12:03 +01:00
}
2019-10-15 15:57:53 +01:00
public stop() {
2020-11-22 21:10:27 +00:00
if ( this . queue . stop ) {
this . queue . stop ( ) ;
2019-10-15 15:57:53 +01:00
}
}
2020-07-20 18:33:38 +01:00
private onGitLabPayload ( body : IGitLabWebhookEvent ) {
2022-04-07 16:49:03 +01:00
if ( body . object_kind === "merge_request" ) {
const action = ( body as unknown as IGitLabWebhookMREvent ) . object_attributes . action ;
2022-07-07 17:47:27 +01:00
if ( ! action ) {
log . warn ( "Got gitlab.merge_request but no action field, which usually means someone pressed the test webhooks button." ) ;
return null ;
}
2022-04-07 16:49:03 +01:00
return ` gitlab.merge_request. ${ action } ` ;
} else if ( body . object_kind === "issue" ) {
const action = ( body as unknown as IGitLabWebhookIssueStateEvent ) . object_attributes . action ;
2022-07-07 17:47:27 +01:00
if ( ! action ) {
log . warn ( "Got gitlab.issue but no action field, which usually means someone pressed the test webhooks button." ) ;
return null ;
}
2022-04-07 16:49:03 +01:00
return ` gitlab.issue. ${ action } ` ;
} else if ( body . object_kind === "note" ) {
2020-09-30 23:03:50 +01:00
return ` gitlab.note.created ` ;
2021-10-08 18:07:18 +01:00
} else if ( body . object_kind === "tag_push" ) {
return "gitlab.tag_push" ;
2021-12-23 15:08:49 +00:00
} else if ( body . object_kind === "wiki_page" ) {
return "gitlab.wiki_page" ;
2022-04-07 16:49:03 +01:00
} else if ( body . object_kind === "release" ) {
const action = ( body as unknown as IGitLabWebhookReleaseEvent ) . action ;
2022-07-07 17:47:27 +01:00
if ( ! action ) {
log . warn ( "Got gitlab.release but no action field, which usually means someone pressed the test webhooks button." ) ;
return null ;
}
2022-04-07 16:49:03 +01:00
return ` gitlab.release. ${ action } ` ;
2022-04-13 15:55:05 +01:00
} else if ( body . object_kind === "push" ) {
return ` gitlab.push ` ;
2020-09-30 23:03:50 +01:00
} else {
return null ;
2020-07-20 18:33:38 +01:00
}
}
2021-10-08 18:07:18 +01:00
private onJiraPayload ( body : IJiraWebhookEvent ) {
2022-10-21 09:16:00 -04:00
body . webhookEvent = body . webhookEvent . replace ( "jira:" , "" ) ;
log . debug ( ` onJiraPayload ${ body . webhookEvent } : ` , body ) ;
return ` jira. ${ body . webhookEvent } ` ;
2021-10-08 18:07:18 +01:00
}
2021-05-02 17:40:48 +01:00
private async onGitHubPayload ( { id , name , payload } : EmitterWebhookEvent ) {
const action = ( payload as unknown as { action : string | undefined } ) . action ;
2021-11-28 14:58:28 +00:00
const eventName = ` github. ${ name } ${ action ? ` . ${ action } ` : "" } ` ;
2022-09-09 09:31:25 +01:00
log . debug ( ` Got GitHub webhook event ${ id } ${ eventName } ` , payload ) ;
2021-05-02 17:40:48 +01:00
try {
await this . queue . push ( {
2021-11-28 14:58:28 +00:00
eventName ,
2021-05-02 17:40:48 +01:00
sender : "Webhooks" ,
data : payload ,
} ) ;
} catch ( err ) {
log . error ( ` Failed to emit payload ${ id } : ${ err } ` ) ;
}
}
2024-01-02 15:52:43 +00:00
private onPayload ( req : WebhooksExpressRequest , res : Response ) {
2019-08-02 19:12:03 +01:00
try {
2020-07-20 18:33:38 +01:00
let eventName : string | null = null ;
2020-11-22 21:10:27 +00:00
const body = req . body ;
2022-08-12 10:59:03 +01:00
const githubGuid = req . headers [ 'x-github-delivery' ] as string | undefined ;
if ( githubGuid ) {
2021-04-25 19:40:02 +01:00
if ( ! this . ghWebhooks ) {
log . warn ( ` Not configured for GitHub webhooks, but got a GitHub event ` )
res . sendStatus ( 500 ) ;
return ;
}
res . sendStatus ( 200 ) ;
2022-08-12 10:59:03 +01:00
if ( this . handledGuids . has ( githubGuid ) ) {
return ;
}
this . handledGuids . set ( githubGuid ) ;
2024-01-02 15:52:43 +00:00
const githubData = req . github as GitHubRequestData ;
if ( ! githubData ) {
throw Error ( 'Expected github data to be set on request' ) ;
}
2021-04-25 19:40:02 +01:00
this . ghWebhooks . verifyAndReceive ( {
2022-08-12 10:59:03 +01:00
id : githubGuid as string ,
2024-11-28 15:04:01 +00:00
name : req.headers [ "x-github-event" ] as WebhookEventName ,
2024-01-02 15:52:43 +00:00
payload : githubData.payload ,
signature : githubData.signature ,
2021-04-25 19:40:02 +01:00
} ) . catch ( ( err ) = > {
log . error ( ` Failed handle GitHubEvent: ${ err } ` ) ;
} ) ;
2021-05-02 17:09:17 +01:00
return ;
2020-07-20 18:33:38 +01:00
} else if ( req . headers [ 'x-gitlab-token' ] ) {
2021-04-25 19:40:02 +01:00
res . sendStatus ( 200 ) ;
2020-07-20 18:33:38 +01:00
eventName = this . onGitLabPayload ( body ) ;
2022-03-04 14:34:44 +00:00
} else if ( JiraWebhooksRouter . IsJIRARequest ( req ) ) {
2021-10-08 18:07:18 +01:00
res . sendStatus ( 200 ) ;
eventName = this . onJiraPayload ( body ) ;
2019-08-07 18:29:18 +01:00
}
if ( eventName ) {
2019-08-06 15:04:39 +01:00
this . queue . push ( {
2019-08-07 18:29:18 +01:00
eventName ,
2019-08-06 15:04:39 +01:00
sender : "GithubWebhooks" ,
data : body ,
2020-02-25 13:25:21 +00:00
} ) . catch ( ( err ) = > {
2021-04-25 19:40:02 +01:00
log . error ( ` Failed to emit payload: ${ err } ` ) ;
2019-08-06 15:04:39 +01:00
} ) ;
2020-07-20 18:33:38 +01:00
} else {
log . debug ( "Unknown event:" , req . body ) ;
2019-08-02 19:12:03 +01:00
}
} catch ( ex ) {
2021-05-02 17:09:17 +01:00
log . error ( "Failed to emit message" , ex ) ;
2019-08-02 19:12:03 +01:00
}
}
2023-03-14 10:50:46 +00:00
public async onGitHubGetOauth ( req : Request < unknown , unknown , unknown , { error ? : string , error_description ? : string , code ? : string , state ? : string , setup_action ? : 'install' } > , res : Response ) {
2023-03-21 10:46:08 +00:00
const oauthResultParams : OAuthPageParams = {
service : "github"
} ;
2023-03-14 10:50:46 +00:00
const { setup_action , state } = req . query ;
log . info ( "Got new oauth request" , { state , setup_action } ) ;
2019-08-08 15:36:44 +01:00
try {
2021-12-01 10:51:49 +00:00
if ( ! this . config . github || ! this . config . github . oauth ) {
2023-03-14 10:50:46 +00:00
throw new ApiError ( 'Bridge is not configured with OAuth support' , ErrCode . DisabledFeature ) ;
2021-12-01 17:22:48 +00:00
}
if ( req . query . error ) {
2023-03-14 10:50:46 +00:00
throw new ApiError ( ` GitHub Error: ${ req . query . error } ${ req . query . error_description } ` , ErrCode . Unknown ) ;
2020-09-30 21:56:56 +01:00
}
2023-03-21 10:46:08 +00:00
if ( setup_action === 'install' ) {
// GitHub App successful install.
oauthResultParams [ "oauth-kind" ] = 'organisation' ;
oauthResultParams . result = "success" ;
} else if ( setup_action === 'request' ) {
// GitHub App install is pending
oauthResultParams [ "oauth-kind" ] = 'organisation' ;
oauthResultParams . result = "pending" ;
} else if ( setup_action ) {
// GitHub App install is in another, unknown state.
oauthResultParams [ "oauth-kind" ] = 'organisation' ;
oauthResultParams . result = setup_action ;
}
else {
// This is a user account setup flow.
oauthResultParams [ 'oauth-kind' ] = "account" ;
2023-03-14 10:50:46 +00:00
if ( ! state ) {
throw new ApiError ( ` Missing state ` , ErrCode . BadValue ) ;
}
if ( ! req . query . code ) {
throw new ApiError ( ` Missing code ` , ErrCode . BadValue ) ;
}
const exists = await this . queue . pushWait < OAuthRequest , boolean > ( {
eventName : "github.oauth.response" ,
sender : "GithubWebhooks" ,
data : {
state ,
} ,
} ) ;
if ( ! exists ) {
throw new ApiError ( ` Could not find user which authorised this request. Has it timed out? ` , undefined , 404 ) ;
}
const accessTokenUrl = GithubInstance . generateOAuthUrl ( this . config . github . baseUrl , "access_token" , {
client_id : this.config.github.oauth.client_id ,
client_secret : this.config.github.oauth.client_secret ,
code : req.query.code as string ,
redirect_uri : this.config.github.oauth.redirect_uri ,
state : req.query.state as string ,
} ) ;
const accessTokenRes = await axios . post ( accessTokenUrl ) ;
const result = qs . parse ( accessTokenRes . data ) as GitHubOAuthTokenResponse | { error : string , error_description : string , error_uri : string } ;
if ( "error" in result ) {
throw new ApiError ( ` GitHub Error: ${ result . error } ${ result . error_description } ` , ErrCode . Unknown ) ;
}
2023-03-21 10:46:08 +00:00
oauthResultParams . result = 'success' ;
2023-03-14 10:50:46 +00:00
await this . queue . push < GitHubOAuthTokenResponse > ( {
eventName : "github.oauth.tokens" ,
sender : "GithubWebhooks" ,
data : { . . . result , state : req.query.state as string } ,
} ) ;
2019-08-08 15:36:44 +01:00
}
2023-03-14 10:50:46 +00:00
} catch ( ex ) {
if ( ex instanceof ApiError ) {
2023-03-21 10:46:08 +00:00
oauthResultParams . result = 'error' ;
oauthResultParams . error = ex . error ;
oauthResultParams . errcode = ex . errcode ;
2023-03-14 10:50:46 +00:00
} else {
log . error ( "Failed to handle oauth request:" , ex ) ;
return res . status ( 500 ) . send ( 'Failed to handle oauth request' ) ;
2021-12-06 18:42:53 +00:00
}
2023-03-14 10:50:46 +00:00
}
2023-03-21 10:46:08 +00:00
const oauthUrl = this . config . widgets && new URL ( "oauth.html" , this . config . widgets . parsedPublicUrl ) ;
2023-03-14 10:50:46 +00:00
if ( oauthUrl ) {
// If we're serving widgets, do something prettier.
2023-03-21 10:46:08 +00:00
Object . entries ( oauthResultParams ) . forEach ( ( [ key , value ] ) = > oauthUrl . searchParams . set ( key , value ) ) ;
2023-03-14 10:50:46 +00:00
return res . redirect ( oauthUrl . toString ( ) ) ;
} else {
2023-03-21 10:46:08 +00:00
if ( oauthResultParams . result === 'success' ) {
return res . send ( ` <p> Your ${ oauthResultParams . service } ${ oauthResultParams [ "oauth-kind" ] } has been bridged </p> ` ) ;
} else if ( oauthResultParams . result === 'error' ) {
return res . status ( 500 ) . send ( ` <p> There was an error bridging your ${ oauthResultParams . service } ${ oauthResultParams [ "oauth-kind" ] } . ${ oauthResultParams . error } ${ oauthResultParams . errcode } </p> ` ) ;
} else {
return res . status ( 500 ) . send ( ` <p> Your ${ oauthResultParams . service } ${ oauthResultParams [ "oauth-kind" ] } is in state ${ oauthResultParams . result } </p> ` ) ;
}
2019-08-08 15:36:44 +01:00
}
}
2024-01-02 15:52:43 +00:00
private verifyRequest ( req : WebhooksExpressRequest , res : Response , buffer : Buffer , encoding : BufferEncoding ) {
2020-07-20 01:34:08 +01:00
if ( req . headers [ 'x-gitlab-token' ] ) {
2021-04-25 19:40:02 +01:00
// GitLab
2020-07-20 01:34:08 +01:00
if ( ! this . config . gitlab ) {
log . error ( "Got a GitLab webhook, but the bridge is not set up for it." ) ;
res . sendStatus ( 400 ) ;
throw Error ( 'Not expecting a gitlab request!' ) ;
}
if ( req . headers [ 'x-gitlab-token' ] === this . config . gitlab . webhook . secret ) {
log . debug ( 'Verified GitLab request' ) ;
return true ;
} else {
log . error ( ` ${ req . url } had an invalid signature ` ) ;
res . sendStatus ( 403 ) ;
throw Error ( "Invalid signature." ) ;
}
2024-01-02 15:52:43 +00:00
} else if ( req . headers [ "x-hub-signature-256" ] && this . ghWebhooks ) {
2021-04-25 19:40:02 +01:00
// GitHub
2024-01-02 15:52:43 +00:00
if ( typeof req . headers [ "x-hub-signature-256" ] !== "string" ) {
throw new ApiError ( "Unexpected multiple headers for x-hub-signature-256" , ErrCode . BadValue , 400 ) ;
}
let jsonStr ;
try {
jsonStr = buffer . toString ( encoding )
} catch ( ex ) {
throw new ApiError ( "Could not decode buffer" , ErrCode . BadValue , 400 ) ;
}
req . github = {
payload : jsonStr ,
signature : req.headers [ "x-hub-signature-256" ]
} ;
2021-04-25 19:40:02 +01:00
return true ;
2022-03-04 14:34:44 +00:00
} else if ( JiraWebhooksRouter . IsJIRARequest ( req ) ) {
2021-10-08 18:07:18 +01:00
// JIRA
if ( ! this . config . jira ) {
log . error ( "Got a JIRA webhook, but the bridge is not set up for it." ) ;
res . sendStatus ( 400 ) ;
throw Error ( 'Not expecting a jira request!' ) ;
}
if ( req . query . secret !== this . config . jira . webhook . secret ) {
log . error ( ` ${ req . url } had an invalid signature ` ) ;
res . sendStatus ( 403 ) ;
throw Error ( "Invalid signature." ) ;
}
return true ;
2019-08-02 19:12:03 +01:00
}
2020-07-20 01:34:08 +01:00
log . error ( ` No signature on URL. Rejecting ` ) ;
res . sendStatus ( 400 ) ;
throw Error ( "Invalid signature." ) ;
2019-08-02 19:12:03 +01:00
}
2019-08-06 18:40:15 +01:00
}