Skip to content

Commit

Permalink
feat: support authorized chainhook events
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed Mar 3, 2023
1 parent a3acde4 commit a2ff106
Show file tree
Hide file tree
Showing 9 changed files with 59 additions and 9 deletions.
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# STACKS_API_ENDPOINT=
# STACKS_EXPLORER_ENDPOINT=
# See src/env.ts for environment variable documentation.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion src/chainhook/server.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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}`,
},
},
},
Expand All @@ -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<Record<never, never>, 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();
Expand Down
7 changes: 6 additions & 1 deletion src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
1 change: 0 additions & 1 deletion tests/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 0 additions & 1 deletion tests/inscriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 0 additions & 1 deletion tests/sats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
31 changes: 30 additions & 1 deletion tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ describe('EventServer', () => {
let db: PgStore;

beforeEach(async () => {
ENV.PGDATABASE = 'postgres';
db = await PgStore.connect({ skipMigrations: true });
await cycleMigrations();
});
Expand Down Expand Up @@ -45,13 +44,41 @@ describe('EventServer', () => {
await fastify.inject({
method: 'POST',
url: '/chainhook/inscription_revealed',
headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` },
payload,
});

await fastify.close();
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', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// ts-unused-exports:disable-next-line
export default (): void => {
process.env.CHAINHOOK_NODE_AUTH_TOKEN = 'test';
process.env.PGDATABASE = 'postgres';
};

0 comments on commit a2ff106

Please sign in to comment.