Skip to content

Commit f6466c3

Browse files
committed
refactor(middleware): all routes use middleware api and expose capability via ANS-101 impl ar-io#27
1 parent fc7b01a commit f6466c3

File tree

8 files changed

+451
-204
lines changed

8 files changed

+451
-204
lines changed

src/app.ts

+45-142
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,23 @@
1818
import { default as cors } from 'cors';
1919
import express from 'express';
2020
//import * as OpenApiValidator from 'express-openapi-validator';
21-
import promMid from 'express-prometheus-middleware';
2221
import fs from 'node:fs';
23-
import swaggerUi from 'swagger-ui-express';
2422
import YAML from 'yaml';
2523

2624
import * as config from './config.js';
2725
import log from './log.js';
26+
import { addCapability } from './middleware/ANS-101.js';
27+
import { createGatewayApiDocsMiddleware } from './middleware/api-docs.js';
28+
import {
29+
createArIoAdminMiddleware,
30+
createArIoCoreMiddleware,
31+
} from './middleware/ar-io.js';
2832
import { createArnsMiddleware } from './middleware/arns.js';
33+
import { createDataMiddleware } from './middleware/data.js';
34+
import { createGraphQLMiddleware } from './middleware/graphql.js';
35+
import { createMetricsMiddleware } from './middleware/metrics.js';
2936
import { createSandboxMiddleware } from './middleware/sandbox.js';
30-
import {
31-
DATA_PATH_REGEX,
32-
RAW_DATA_PATH_REGEX,
33-
createDataHandler,
34-
createRawDataHandler,
35-
} from './routes/data.js';
36-
import { apolloServer } from './routes/graphql/index.js';
37+
import { ArweaveG8wayMiddleware } from './middleware/types.js';
3738
import * as system from './system.js';
3839

3940
system.arweaveClient.refreshPeers();
@@ -55,148 +56,50 @@ const app = express();
5556

5657
app.use(cors());
5758

58-
app.use(
59-
promMid({
60-
metricsPath: '/ar-io/__gateway_metrics',
61-
extraMasks: [
62-
// Mask all paths except for the ones below
63-
/^(?!api-docs)(?!ar-io)(?!graphql)(?!openapi\.json)(?!raw).+$/,
64-
// Mask Arweave TX IDs
65-
/[a-zA-Z0-9_-]{43}/,
66-
],
67-
}),
68-
);
69-
70-
const dataHandler = createDataHandler({
71-
log,
72-
dataIndex: system.contiguousDataIndex,
73-
dataSource: system.contiguousDataSource,
74-
blockListValidator: system.blockListValidator,
75-
manifestPathResolver: system.manifestPathResolver,
76-
});
77-
78-
app.use(
59+
const coreG8wayMiddleware = [
60+
createMetricsMiddleware(),
7961
createArnsMiddleware({
80-
dataHandler,
62+
log,
63+
dataIndex: system.contiguousDataIndex,
64+
dataSource: system.contiguousDataSource,
65+
blockListValidator: system.blockListValidator,
66+
manifestPathResolver: system.manifestPathResolver,
8167
nameResolver: system.nameResolver,
8268
}),
83-
);
84-
85-
app.use(
8669
createSandboxMiddleware({
8770
rootHost: config.ARNS_ROOT_HOST,
8871
sandboxProtocol: config.SANDBOX_PROTOCOL,
8972
}),
90-
);
91-
92-
// OpenAPI Spec
93-
const openapiDocument = YAML.parse(
94-
fs.readFileSync('docs/openapi.yaml', 'utf8'),
95-
);
96-
app.get('/openapi.json', (_req, res) => {
97-
res.json(openapiDocument);
98-
});
99-
100-
// Swagger UI
101-
const options = {
102-
explorer: true,
103-
};
104-
app.use(
105-
'/api-docs',
106-
swaggerUi.serve,
107-
swaggerUi.setup(openapiDocument, options),
108-
);
109-
110-
// Healthcheck
111-
app.get('/ar-io/healthcheck', (_req, res) => {
112-
const data = {
113-
uptime: process.uptime(),
114-
message: 'Welcome to the Permaweb.',
115-
date: new Date(),
116-
};
117-
118-
res.status(200).send(data);
119-
});
120-
121-
// ar.io network info
122-
app.get('/ar-io/info', (_req, res) => {
123-
res.status(200).send({
124-
wallet: config.AR_IO_WALLET,
125-
});
126-
});
127-
128-
// Only allow access to admin routes if the bearer token matches the admin api key
129-
app.use('/ar-io/admin', (req, res, next) => {
130-
if (req.headers.authorization === `Bearer ${config.ADMIN_API_KEY}`) {
131-
next();
132-
} else {
133-
res.status(401).send('Unauthorized');
134-
}
135-
});
136-
137-
// Debug info (for internal use)
138-
app.get('/ar-io/admin/debug', async (_req, res) => {
139-
res.json({
140-
db: await system.db.getDebugInfo(),
141-
});
142-
});
143-
144-
// Block access to contiguous data by ID or hash
145-
app.put('/ar-io/admin/block-data', express.json(), async (req, res) => {
146-
// TODO improve validation
147-
try {
148-
const { id, hash, source, notes } = req.body;
149-
if (id === undefined && hash === undefined) {
150-
res.status(400).send("Must provide 'id' or 'hash'");
151-
return;
152-
}
153-
system.db.blockData({ id, hash, source, notes });
154-
// TODO check return value
155-
res.json({ message: 'Content blocked' });
156-
} catch (error: any) {
157-
res.status(500).send(error?.message);
158-
}
159-
});
160-
161-
// Queue a TX ID for processing
162-
app.post('/ar-io/admin/queue-tx', express.json(), async (req, res) => {
163-
try {
164-
const { id } = req.body;
165-
if (id === undefined) {
166-
res.status(400).send("Must provide 'id'");
167-
return;
168-
}
169-
system.prioritizedTxIds.add(id);
170-
system.txFetcher.queueTxId(id);
171-
res.json({ message: 'TX queued' });
172-
} catch (error: any) {
173-
res.status(500).send(error?.message);
174-
}
175-
});
176-
177-
// GraphQL
178-
const apolloServerInstanceGql = apolloServer(system.db, {
179-
introspection: true,
180-
});
181-
apolloServerInstanceGql.start().then(() => {
182-
apolloServerInstanceGql.applyMiddleware({
183-
app,
184-
path: '/graphql',
185-
});
186-
app.listen(config.PORT, () => {
187-
log.info(`Listening on port ${config.PORT}`);
188-
});
189-
});
190-
191-
// Data routes
192-
app.get(
193-
RAW_DATA_PATH_REGEX,
194-
createRawDataHandler({
73+
createGatewayApiDocsMiddleware({
74+
openapiDocument: YAML.parse(fs.readFileSync('docs/openapi.yaml', 'utf8')),
75+
}),
76+
// TODO: use config schema to parse config for correctness instead of trusting types.
77+
createArIoCoreMiddleware({ AR_IO_WALLET: config.AR_IO_WALLET as string }),
78+
createArIoAdminMiddleware({
79+
db: system.db,
80+
prioritizedTxIds: system.prioritizedTxIds,
81+
txFetcher: system.txFetcher,
82+
ADMIN_API_KEY: config.ADMIN_API_KEY,
83+
}),
84+
createGraphQLMiddleware(system),
85+
createDataMiddleware({
19586
log,
19687
dataIndex: system.contiguousDataIndex,
19788
dataSource: system.contiguousDataSource,
19889
blockListValidator: system.blockListValidator,
90+
manifestPathResolver: system.manifestPathResolver,
19991
}),
200-
);
201-
202-
app.get(DATA_PATH_REGEX, dataHandler);
92+
];
93+
94+
// TODO: implement dynamically importing middleware
95+
const dynamicallyAddedG8wayMiddleware: Promise<ArweaveG8wayMiddleware>[] = [];
96+
97+
[...coreG8wayMiddleware]
98+
.reduce(
99+
($app, middleware) =>
100+
middleware({ addCapability }).then(async (m) => m(await $app)),
101+
Promise.resolve(app),
102+
)
103+
.then((app) => {
104+
app.listen(config.PORT, () => log.info(`Listening on port ${config.PORT}`));
105+
});

src/middleware/api-docs.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* AR.IO Gateway
3+
* Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
import swaggerUi from 'swagger-ui-express';
19+
20+
import type { ArweaveG8wayMiddleware } from './types.js';
21+
22+
export const createGatewayApiDocsMiddleware: (coreDeps: {
23+
openapiDocument: any;
24+
}) => ArweaveG8wayMiddleware =
25+
({ openapiDocument }) =>
26+
async ({ addCapability }) => {
27+
await addCapability({ name: 'gateway-api-docs', version: '1.0.0' });
28+
29+
return async (app) => {
30+
// OpenAPI
31+
app.get('/openapi.json', (_req, res) => res.json(openapiDocument));
32+
33+
// Swagger UI
34+
app.use(
35+
'/api-docs',
36+
swaggerUi.serve,
37+
swaggerUi.setup(openapiDocument, { explorer: true }),
38+
);
39+
40+
return app;
41+
};
42+
};

src/middleware/ar-io.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* AR.IO Gateway
3+
* Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
import express from 'express';
19+
20+
import type { StandaloneSqliteDatabase } from '../database/standalone-sqlite.js';
21+
import type { TransactionFetcher } from '../workers/transaction-fetcher.js';
22+
import type { ArweaveG8wayMiddleware } from './types.js';
23+
24+
export const createArIoCoreMiddleware: (coreDeps: {
25+
AR_IO_WALLET: string;
26+
}) => ArweaveG8wayMiddleware =
27+
({ AR_IO_WALLET }) =>
28+
async ({ addCapability }) => {
29+
await addCapability({ name: 'gateway-ar-core', version: '1.0.0' });
30+
31+
return async (app) => {
32+
// Healthcheck
33+
app.get('/ar-io/healthcheck', (_req, res) => {
34+
const data = {
35+
uptime: process.uptime(),
36+
message: 'Welcome to the Permaweb.',
37+
date: new Date(),
38+
};
39+
40+
res.status(200).send(data);
41+
});
42+
43+
// ar.io network info
44+
app.get('/ar-io/info', (_req, res) => {
45+
res.status(200).send({
46+
wallet: AR_IO_WALLET,
47+
});
48+
});
49+
50+
return app;
51+
};
52+
};
53+
54+
export const createArIoAdminMiddleware: (coreDeps: {
55+
db: StandaloneSqliteDatabase;
56+
prioritizedTxIds: Set<string>;
57+
txFetcher: TransactionFetcher;
58+
ADMIN_API_KEY: string;
59+
}) => ArweaveG8wayMiddleware =
60+
({ db, prioritizedTxIds, txFetcher, ADMIN_API_KEY }) =>
61+
async ({ addCapability }) => {
62+
await addCapability({ name: 'ar-io-admin', version: '1.0.0' });
63+
64+
return async (app) => {
65+
// Only allow access to admin routes if the bearer token matches the admin api key
66+
app.use('/ar-io/admin', (req, res, next) => {
67+
if (req.headers.authorization === `Bearer ${ADMIN_API_KEY}`) {
68+
next();
69+
} else {
70+
res.status(401).send('Unauthorized');
71+
}
72+
});
73+
74+
// Debug info (for internal use)
75+
app.get('/ar-io/admin/debug', async (_req, res) => {
76+
res.json({
77+
db: await db.getDebugInfo(),
78+
});
79+
});
80+
81+
// Block access to contiguous data by ID or hash
82+
app.put('/ar-io/admin/block-data', express.json(), async (req, res) => {
83+
// TODO improve validation
84+
try {
85+
const { id, hash, source, notes } = req.body;
86+
if (id === undefined && hash === undefined) {
87+
res.status(400).send("Must provide 'id' or 'hash'");
88+
return;
89+
}
90+
db.blockData({ id, hash, source, notes });
91+
// TODO check return value
92+
res.json({ message: 'Content blocked' });
93+
} catch (error: any) {
94+
res.status(500).send(error?.message);
95+
}
96+
});
97+
98+
// Queue a TX ID for processing
99+
app.post('/ar-io/admin/queue-tx', express.json(), async (req, res) => {
100+
try {
101+
const { id } = req.body;
102+
if (id === undefined) {
103+
res.status(400).send("Must provide 'id'");
104+
return;
105+
}
106+
prioritizedTxIds.add(id);
107+
txFetcher.queueTxId(id);
108+
res.json({ message: 'TX queued' });
109+
} catch (error: any) {
110+
res.status(500).send(error?.message);
111+
}
112+
});
113+
114+
return app;
115+
};
116+
};

0 commit comments

Comments
 (0)