refactor: webhook handler to improve readability and error handling
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
5
packages/webhook/src/error.ts
Normal file
5
packages/webhook/src/error.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export class InvalidSignatureError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid Signature')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,15 @@ import type { IncomingMessage, ServerResponse } from 'http'
|
|||||||
import type { BusinessEvents } from './event.enum'
|
import type { BusinessEvents } from './event.enum'
|
||||||
import type { ExtendedEventEmitter } from './types'
|
import type { ExtendedEventEmitter } from './types'
|
||||||
|
|
||||||
|
import { InvalidSignatureError } from './error'
|
||||||
|
|
||||||
interface CreateHandlerOptions {
|
interface CreateHandlerOptions {
|
||||||
secret: string
|
secret: string
|
||||||
events?: 'all' | BusinessEvents[]
|
events?: 'all' | BusinessEvents[]
|
||||||
}
|
}
|
||||||
|
export type RequestWithJSONBody = IncomingMessage & Request & { body: object }
|
||||||
type Handler = {
|
type Handler = {
|
||||||
(req: IncomingMessage, res: ServerResponse): void
|
(req: RequestWithJSONBody, res: ServerResponse): void
|
||||||
} & {
|
} & {
|
||||||
emitter: ExtendedEventEmitter
|
emitter: ExtendedEventEmitter
|
||||||
}
|
}
|
||||||
@@ -21,24 +23,10 @@ export const createHandler = (options: CreateHandlerOptions): Handler => {
|
|||||||
|
|
||||||
const handler: Handler = async function (req, res) {
|
const handler: Handler = async function (req, res) {
|
||||||
try {
|
try {
|
||||||
const signature = req.headers['x-webhook-signature']
|
const data = await readDataFromRequest({ req, secret })
|
||||||
assert(
|
|
||||||
typeof signature === 'string',
|
const { event, payload } = data
|
||||||
'X-Webhook-Signature must be string',
|
|
||||||
)
|
|
||||||
const event = req.headers['x-webhook-event']
|
|
||||||
const signature256 = req.headers['x-webhook-signature256']
|
|
||||||
assert(
|
|
||||||
typeof signature256 === 'string',
|
|
||||||
'X-Webhook-Signature256 must be string',
|
|
||||||
)
|
|
||||||
|
|
||||||
const obj = (req as any).body || (await parseJSONFromRequest(req))
|
|
||||||
const stringifyPayload = JSON.stringify(obj)
|
|
||||||
const isValid =
|
|
||||||
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
|
|
||||||
verifyWebhookSha1(secret, stringifyPayload, signature as string)
|
|
||||||
if (isValid) {
|
|
||||||
if (event === 'health_check') {
|
if (event === 'health_check') {
|
||||||
res.statusCode = 200
|
res.statusCode = 200
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
@@ -46,23 +34,23 @@ export const createHandler = (options: CreateHandlerOptions): Handler => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.emitter.emit(event as BusinessEvents, obj)
|
handler.emitter.emit(event as BusinessEvents, payload)
|
||||||
handler.emitter.emit('*', {
|
handler.emitter.emit('*', {
|
||||||
type: event,
|
type: event,
|
||||||
payload: obj,
|
payload,
|
||||||
})
|
})
|
||||||
res.statusCode = 200
|
res.statusCode = 200
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.end(JSON.stringify({ ok: 1 }))
|
res.end(JSON.stringify({ ok: 1 }))
|
||||||
} else {
|
} catch (err) {
|
||||||
console.error('revice a invalidate webhook payload', req.headers)
|
if (err instanceof InvalidSignatureError) {
|
||||||
handler.emitter.emit('error', new Error('invalidate signature'))
|
handler.emitter.emit('error', new Error('invalidate signature'))
|
||||||
|
|
||||||
res.statusCode = 400
|
res.statusCode = 400
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.end(JSON.stringify({ ok: 0, message: 'Invalid Signature' }))
|
res.end(JSON.stringify({ ok: 0, message: 'Invalid Signature' }))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
res.statusCode = 500
|
res.statusCode = 500
|
||||||
res.end(JSON.stringify({ ok: 0, message: err.message }))
|
res.end(JSON.stringify({ ok: 0, message: err.message }))
|
||||||
}
|
}
|
||||||
@@ -72,20 +60,37 @@ export const createHandler = (options: CreateHandlerOptions): Handler => {
|
|||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJSONFromRequest(req: IncomingMessage) {
|
export const readDataFromRequest = async ({
|
||||||
return new Promise((resolve, reject) => {
|
req,
|
||||||
let body = ''
|
secret,
|
||||||
req.on('data', (chunk) => {
|
}: {
|
||||||
body += chunk
|
secret: string
|
||||||
})
|
req: RequestWithJSONBody
|
||||||
req.on('end', () => {
|
}) => {
|
||||||
try {
|
const signature = req.headers['x-webhook-signature']
|
||||||
resolve(JSON.parse(body))
|
assert(typeof signature === 'string', 'X-Webhook-Signature must be string')
|
||||||
} catch (err) {
|
const event = req.headers['x-webhook-event']
|
||||||
reject(err)
|
const signature256 = req.headers['x-webhook-signature256']
|
||||||
|
assert(
|
||||||
|
typeof signature256 === 'string',
|
||||||
|
'X-Webhook-Signature256 must be string',
|
||||||
|
)
|
||||||
|
|
||||||
|
const obj = req.body
|
||||||
|
const stringifyPayload = JSON.stringify(obj)
|
||||||
|
const isValid =
|
||||||
|
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
|
||||||
|
verifyWebhookSha1(secret, stringifyPayload, signature as string)
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
payload: obj,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('revice a invalidate webhook payload', req.headers)
|
||||||
|
throw new InvalidSignatureError()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyWebhook(
|
export function verifyWebhook(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './event.enum'
|
export * from './event.enum'
|
||||||
export * from './handler'
|
export * from './handler'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
export * from './error'
|
||||||
|
|||||||
Reference in New Issue
Block a user