118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import assert from 'assert'
|
|
import { createHmac, timingSafeEqual } from 'crypto'
|
|
import { EventEmitter } from 'events'
|
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
import type { BusinessEvents } from './event.enum'
|
|
import type { ExtendedEventEmitter, GenericEvent } from './types'
|
|
|
|
import { InvalidSignatureError } from './error'
|
|
|
|
interface CreateHandlerOptions {
|
|
secret: string
|
|
// events?: 'all' | BusinessEvents[]
|
|
}
|
|
export type RequestWithJSONBody = IncomingMessage & Request & { body: object }
|
|
type Handler = {
|
|
(req: RequestWithJSONBody, res: ServerResponse): void
|
|
} & {
|
|
emitter: ExtendedEventEmitter
|
|
}
|
|
|
|
export const createHandler = (options: CreateHandlerOptions): Handler => {
|
|
const { secret } = options
|
|
|
|
const handler: Handler = async function (req, res) {
|
|
try {
|
|
const data = await readDataFromRequest({ req, secret })
|
|
|
|
const { type: event, payload } = data
|
|
|
|
handler.emitter.emit(event as any, payload)
|
|
handler.emitter.emit('*', {
|
|
type: event,
|
|
payload,
|
|
})
|
|
res.statusCode = 200
|
|
res.setHeader('Content-Type', 'application/json')
|
|
res.end(JSON.stringify({ ok: 1 }))
|
|
} catch (err) {
|
|
if (err instanceof InvalidSignatureError) {
|
|
handler.emitter.emit('error', new Error('invalidate signature'))
|
|
|
|
res.statusCode = 400
|
|
res.setHeader('Content-Type', 'application/json')
|
|
res.end(JSON.stringify({ ok: 0, message: 'Invalid Signature' }))
|
|
return
|
|
}
|
|
res.statusCode = 500
|
|
res.end(JSON.stringify({ ok: 0, message: err.message }))
|
|
}
|
|
}
|
|
|
|
handler.emitter = new EventEmitter()
|
|
return handler
|
|
}
|
|
|
|
export const readDataFromRequest = async ({
|
|
req,
|
|
secret,
|
|
}: {
|
|
secret: string
|
|
req: RequestWithJSONBody
|
|
}) => {
|
|
const signature = req.headers['x-webhook-signature']
|
|
assert(typeof signature === 'string', '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.body
|
|
const stringifyPayload = JSON.stringify(obj)
|
|
const isValid =
|
|
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
|
|
verifyWebhookSha1(secret, stringifyPayload, signature as string)
|
|
|
|
if (isValid) {
|
|
return {
|
|
type: event as BusinessEvents,
|
|
payload: obj as any,
|
|
} as GenericEvent
|
|
} else {
|
|
console.error('revice a invalidate webhook payload', req.headers)
|
|
throw new InvalidSignatureError()
|
|
}
|
|
}
|
|
|
|
export function verifyWebhook(
|
|
secret: string,
|
|
payload: string,
|
|
receivedSignature: string,
|
|
): boolean {
|
|
const hmac = createHmac('sha256', secret)
|
|
hmac.update(payload)
|
|
|
|
// 安全地比较两个签名,以防止时间攻击
|
|
return timingSafeEqual(
|
|
Buffer.from(receivedSignature),
|
|
Buffer.from(hmac.digest('hex')),
|
|
)
|
|
}
|
|
|
|
export function verifyWebhookSha1(
|
|
secret: string,
|
|
payload: string,
|
|
receivedSignature: string,
|
|
): boolean {
|
|
const hmac = createHmac('sha1', secret)
|
|
hmac.update(payload)
|
|
|
|
// 安全地比较两个签名,以防止时间攻击
|
|
return timingSafeEqual(
|
|
Buffer.from(receivedSignature),
|
|
Buffer.from(hmac.digest('hex')),
|
|
)
|
|
}
|