From a2ff1065aa4a64ab15d1d74209361707c82e704e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Fri, 3 Mar 2023 11:58:13 -0600 Subject: [PATCH] feat: support authorized chainhook events --- .env.example | 3 +-- jest.config.js | 2 +- src/chainhook/server.ts | 17 ++++++++++++++++- src/env.ts | 7 ++++++- tests/cache.test.ts | 1 - tests/inscriptions.test.ts | 1 - tests/sats.test.ts | 1 - tests/server.test.ts | 31 ++++++++++++++++++++++++++++++- tests/setup.ts | 5 +++++ 9 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 tests/setup.ts diff --git a/.env.example b/.env.example index b0ff0a80..8360c471 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ -# STACKS_API_ENDPOINT= -# STACKS_EXPLORER_ENDPOINT= +# See src/env.ts for environment variable documentation. diff --git a/jest.config.js b/jest.config.js index 54dec25e..2773d5c8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -59,7 +59,7 @@ module.exports = { // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, + globalSetup: './setup.ts', // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, diff --git a/src/chainhook/server.ts b/src/chainhook/server.ts index 4a4991b1..f6e6bfd0 100644 --- a/src/chainhook/server.ts +++ b/src/chainhook/server.ts @@ -1,6 +1,6 @@ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { randomUUID } from 'crypto'; -import Fastify, { FastifyPluginCallback } from 'fastify'; +import Fastify, { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify'; import { Server } from 'http'; import { request } from 'undici'; import { ENV } from '../env'; @@ -47,6 +47,7 @@ async function registerChainhookPredicates() { then_that: { http_post: { url: `http://${ENV.EXTERNAL_HOSTNAME}/chainhook/inscription_revealed`, + authorization_header: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}`, }, }, }, @@ -71,11 +72,25 @@ async function removeChainhookPredicates() { logger.info(`EventServer removed "inscription_revealed" predicate (${REVEAL__PREDICATE_UUID})`); } +/** + * Check that incoming chainhook requests are properly authorized. + * @param request - Fastify request + * @param reply - Fastify reply + */ +async function isAuthorizedChainhookEvent(request: FastifyRequest, reply: FastifyReply) { + const authHeader = request.headers.authorization; + if (authHeader && authHeader === `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}`) { + return; + } + await reply.code(403).send(); +} + const Chainhook: FastifyPluginCallback, Server, TypeBoxTypeProvider> = ( fastify, options, done ) => { + fastify.addHook('preHandler', isAuthorizedChainhookEvent); fastify.post('/chainhook/inscription_revealed', async (request, reply) => { await processInscriptionRevealed(request.body, fastify.db); await reply.code(200).send(); diff --git a/src/env.ts b/src/env.ts index 33c4c1aa..ac9c6319 100644 --- a/src/env.ts +++ b/src/env.ts @@ -33,8 +33,13 @@ const schema = Type.Object({ /** Hostname of the chainhook node we'll use to register predicates */ CHAINHOOK_NODE_RPC_HOST: Type.String({ default: '127.0.0.1' }), - /** Port of the chainhook node */ + /** Control port of the chainhook node */ CHAINHOOK_NODE_RPC_PORT: Type.Number({ default: 20456, minimum: 0, maximum: 65535 }), + /** + * Authorization token that the chainhook node must send with every event to make sure it's + * coming from the valid instance + */ + CHAINHOOK_NODE_AUTH_TOKEN: Type.String(), PGHOST: Type.String(), PGPORT: Type.Number({ default: 5432, minimum: 0, maximum: 65535 }), diff --git a/tests/cache.test.ts b/tests/cache.test.ts index 184b633a..0a437d91 100644 --- a/tests/cache.test.ts +++ b/tests/cache.test.ts @@ -9,7 +9,6 @@ describe('ETag cache', () => { let fastify: TestFastifyServer; beforeEach(async () => { - ENV.PGDATABASE = 'postgres'; db = await PgStore.connect({ skipMigrations: true }); fastify = await buildApiServer({ db }); await cycleMigrations(); diff --git a/tests/inscriptions.test.ts b/tests/inscriptions.test.ts index dd20fdae..e3c02740 100644 --- a/tests/inscriptions.test.ts +++ b/tests/inscriptions.test.ts @@ -9,7 +9,6 @@ describe('/inscriptions', () => { let fastify: TestFastifyServer; beforeEach(async () => { - ENV.PGDATABASE = 'postgres'; db = await PgStore.connect({ skipMigrations: true }); fastify = await buildApiServer({ db }); await cycleMigrations(); diff --git a/tests/sats.test.ts b/tests/sats.test.ts index 26102844..91cccc35 100644 --- a/tests/sats.test.ts +++ b/tests/sats.test.ts @@ -9,7 +9,6 @@ describe('/sats', () => { let fastify: TestFastifyServer; beforeEach(async () => { - ENV.PGDATABASE = 'postgres'; db = await PgStore.connect({ skipMigrations: true }); fastify = await buildApiServer({ db }); await cycleMigrations(); diff --git a/tests/server.test.ts b/tests/server.test.ts index b242c957..e6f3196c 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -13,7 +13,6 @@ describe('EventServer', () => { let db: PgStore; beforeEach(async () => { - ENV.PGDATABASE = 'postgres'; db = await PgStore.connect({ skipMigrations: true }); await cycleMigrations(); }); @@ -45,6 +44,7 @@ describe('EventServer', () => { await fastify.inject({ method: 'POST', url: '/chainhook/inscription_revealed', + headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload, }); @@ -52,6 +52,33 @@ describe('EventServer', () => { await agent.close(); agent.assertNoPendingInterceptors(); }); + + test('ignores unauthorized events', async () => { + const agent = new MockAgent(); + agent.disableNetConnect(); + const interceptor = agent.get(CHAINHOOK_BASE_PATH); + interceptor.intercept({ path: '/ping', method: 'GET' }).reply(200); + interceptor.intercept({ path: '/v1/chainhooks', method: 'POST' }).reply(200); + interceptor + .intercept({ + path: `/v1/chainhooks/bitcoin/${REVEAL__PREDICATE_UUID}`, + method: 'DELETE', + }) + .reply(200); + setGlobalDispatcher(agent); + + const fastify = await buildChainhookServer({ db }); + const payload = { test: 'value' }; + const response = await fastify.inject({ + method: 'POST', + url: '/chainhook/inscription_revealed', + payload, + }); + expect(response.statusCode).toBe(403); + + await fastify.close(); + await agent.close(); + }); }); describe('parser', () => { @@ -144,6 +171,7 @@ describe('EventServer', () => { const response = await fastify.inject({ method: 'POST', url: '/chainhook/inscription_revealed', + headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload1, }); expect(response.statusCode).toBe(200); @@ -200,6 +228,7 @@ describe('EventServer', () => { const response2 = await fastify.inject({ method: 'POST', url: '/chainhook/inscription_revealed', + headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload2, }); expect(response2.statusCode).toBe(200); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 00000000..980c769f --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,5 @@ +// ts-unused-exports:disable-next-line +export default (): void => { + process.env.CHAINHOOK_NODE_AUTH_TOKEN = 'test'; + process.env.PGDATABASE = 'postgres'; +};