Skip to content

Commit

Permalink
refactor(middleware): all routes use middleware api and expose capabi…
Browse files Browse the repository at this point in the history
…lity via ANS-101 impl ar-io#27
  • Loading branch information
TillaTheHun0 committed Aug 21, 2023
1 parent fc7b01a commit f6466c3
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 204 deletions.
187 changes: 45 additions & 142 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@
import { default as cors } from 'cors';
import express from 'express';
//import * as OpenApiValidator from 'express-openapi-validator';
import promMid from 'express-prometheus-middleware';
import fs from 'node:fs';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yaml';

import * as config from './config.js';
import log from './log.js';
import { addCapability } from './middleware/ANS-101.js';
import { createGatewayApiDocsMiddleware } from './middleware/api-docs.js';
import {
createArIoAdminMiddleware,
createArIoCoreMiddleware,
} from './middleware/ar-io.js';
import { createArnsMiddleware } from './middleware/arns.js';
import { createDataMiddleware } from './middleware/data.js';
import { createGraphQLMiddleware } from './middleware/graphql.js';
import { createMetricsMiddleware } from './middleware/metrics.js';
import { createSandboxMiddleware } from './middleware/sandbox.js';
import {
DATA_PATH_REGEX,
RAW_DATA_PATH_REGEX,
createDataHandler,
createRawDataHandler,
} from './routes/data.js';
import { apolloServer } from './routes/graphql/index.js';
import { ArweaveG8wayMiddleware } from './middleware/types.js';
import * as system from './system.js';

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

app.use(cors());

app.use(
promMid({
metricsPath: '/ar-io/__gateway_metrics',
extraMasks: [
// Mask all paths except for the ones below
/^(?!api-docs)(?!ar-io)(?!graphql)(?!openapi\.json)(?!raw).+$/,
// Mask Arweave TX IDs
/[a-zA-Z0-9_-]{43}/,
],
}),
);

const dataHandler = createDataHandler({
log,
dataIndex: system.contiguousDataIndex,
dataSource: system.contiguousDataSource,
blockListValidator: system.blockListValidator,
manifestPathResolver: system.manifestPathResolver,
});

app.use(
const coreG8wayMiddleware = [
createMetricsMiddleware(),
createArnsMiddleware({
dataHandler,
log,
dataIndex: system.contiguousDataIndex,
dataSource: system.contiguousDataSource,
blockListValidator: system.blockListValidator,
manifestPathResolver: system.manifestPathResolver,
nameResolver: system.nameResolver,
}),
);

app.use(
createSandboxMiddleware({
rootHost: config.ARNS_ROOT_HOST,
sandboxProtocol: config.SANDBOX_PROTOCOL,
}),
);

// OpenAPI Spec
const openapiDocument = YAML.parse(
fs.readFileSync('docs/openapi.yaml', 'utf8'),
);
app.get('/openapi.json', (_req, res) => {
res.json(openapiDocument);
});

// Swagger UI
const options = {
explorer: true,
};
app.use(
'/api-docs',
swaggerUi.serve,
swaggerUi.setup(openapiDocument, options),
);

// Healthcheck
app.get('/ar-io/healthcheck', (_req, res) => {
const data = {
uptime: process.uptime(),
message: 'Welcome to the Permaweb.',
date: new Date(),
};

res.status(200).send(data);
});

// ar.io network info
app.get('/ar-io/info', (_req, res) => {
res.status(200).send({
wallet: config.AR_IO_WALLET,
});
});

// Only allow access to admin routes if the bearer token matches the admin api key
app.use('/ar-io/admin', (req, res, next) => {
if (req.headers.authorization === `Bearer ${config.ADMIN_API_KEY}`) {
next();
} else {
res.status(401).send('Unauthorized');
}
});

// Debug info (for internal use)
app.get('/ar-io/admin/debug', async (_req, res) => {
res.json({
db: await system.db.getDebugInfo(),
});
});

// Block access to contiguous data by ID or hash
app.put('/ar-io/admin/block-data', express.json(), async (req, res) => {
// TODO improve validation
try {
const { id, hash, source, notes } = req.body;
if (id === undefined && hash === undefined) {
res.status(400).send("Must provide 'id' or 'hash'");
return;
}
system.db.blockData({ id, hash, source, notes });
// TODO check return value
res.json({ message: 'Content blocked' });
} catch (error: any) {
res.status(500).send(error?.message);
}
});

// Queue a TX ID for processing
app.post('/ar-io/admin/queue-tx', express.json(), async (req, res) => {
try {
const { id } = req.body;
if (id === undefined) {
res.status(400).send("Must provide 'id'");
return;
}
system.prioritizedTxIds.add(id);
system.txFetcher.queueTxId(id);
res.json({ message: 'TX queued' });
} catch (error: any) {
res.status(500).send(error?.message);
}
});

// GraphQL
const apolloServerInstanceGql = apolloServer(system.db, {
introspection: true,
});
apolloServerInstanceGql.start().then(() => {
apolloServerInstanceGql.applyMiddleware({
app,
path: '/graphql',
});
app.listen(config.PORT, () => {
log.info(`Listening on port ${config.PORT}`);
});
});

// Data routes
app.get(
RAW_DATA_PATH_REGEX,
createRawDataHandler({
createGatewayApiDocsMiddleware({
openapiDocument: YAML.parse(fs.readFileSync('docs/openapi.yaml', 'utf8')),
}),
// TODO: use config schema to parse config for correctness instead of trusting types.
createArIoCoreMiddleware({ AR_IO_WALLET: config.AR_IO_WALLET as string }),
createArIoAdminMiddleware({
db: system.db,
prioritizedTxIds: system.prioritizedTxIds,
txFetcher: system.txFetcher,
ADMIN_API_KEY: config.ADMIN_API_KEY,
}),
createGraphQLMiddleware(system),
createDataMiddleware({
log,
dataIndex: system.contiguousDataIndex,
dataSource: system.contiguousDataSource,
blockListValidator: system.blockListValidator,
manifestPathResolver: system.manifestPathResolver,
}),
);

app.get(DATA_PATH_REGEX, dataHandler);
];

// TODO: implement dynamically importing middleware
const dynamicallyAddedG8wayMiddleware: Promise<ArweaveG8wayMiddleware>[] = [];

[...coreG8wayMiddleware]
.reduce(
($app, middleware) =>
middleware({ addCapability }).then(async (m) => m(await $app)),
Promise.resolve(app),
)
.then((app) => {
app.listen(config.PORT, () => log.info(`Listening on port ${config.PORT}`));
});
42 changes: 42 additions & 0 deletions src/middleware/api-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* AR.IO Gateway
* Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import swaggerUi from 'swagger-ui-express';

import type { ArweaveG8wayMiddleware } from './types.js';

export const createGatewayApiDocsMiddleware: (coreDeps: {
openapiDocument: any;
}) => ArweaveG8wayMiddleware =
({ openapiDocument }) =>
async ({ addCapability }) => {
await addCapability({ name: 'gateway-api-docs', version: '1.0.0' });

return async (app) => {
// OpenAPI
app.get('/openapi.json', (_req, res) => res.json(openapiDocument));

// Swagger UI
app.use(
'/api-docs',
swaggerUi.serve,
swaggerUi.setup(openapiDocument, { explorer: true }),
);

return app;
};
};
116 changes: 116 additions & 0 deletions src/middleware/ar-io.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* AR.IO Gateway
* Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import express from 'express';

import type { StandaloneSqliteDatabase } from '../database/standalone-sqlite.js';
import type { TransactionFetcher } from '../workers/transaction-fetcher.js';
import type { ArweaveG8wayMiddleware } from './types.js';

export const createArIoCoreMiddleware: (coreDeps: {
AR_IO_WALLET: string;
}) => ArweaveG8wayMiddleware =
({ AR_IO_WALLET }) =>
async ({ addCapability }) => {
await addCapability({ name: 'gateway-ar-core', version: '1.0.0' });

return async (app) => {
// Healthcheck
app.get('/ar-io/healthcheck', (_req, res) => {
const data = {
uptime: process.uptime(),
message: 'Welcome to the Permaweb.',
date: new Date(),
};

res.status(200).send(data);
});

// ar.io network info
app.get('/ar-io/info', (_req, res) => {
res.status(200).send({
wallet: AR_IO_WALLET,
});
});

return app;
};
};

export const createArIoAdminMiddleware: (coreDeps: {
db: StandaloneSqliteDatabase;
prioritizedTxIds: Set<string>;
txFetcher: TransactionFetcher;
ADMIN_API_KEY: string;
}) => ArweaveG8wayMiddleware =
({ db, prioritizedTxIds, txFetcher, ADMIN_API_KEY }) =>
async ({ addCapability }) => {
await addCapability({ name: 'ar-io-admin', version: '1.0.0' });

return async (app) => {
// Only allow access to admin routes if the bearer token matches the admin api key
app.use('/ar-io/admin', (req, res, next) => {
if (req.headers.authorization === `Bearer ${ADMIN_API_KEY}`) {
next();
} else {
res.status(401).send('Unauthorized');
}
});

// Debug info (for internal use)
app.get('/ar-io/admin/debug', async (_req, res) => {
res.json({
db: await db.getDebugInfo(),
});
});

// Block access to contiguous data by ID or hash
app.put('/ar-io/admin/block-data', express.json(), async (req, res) => {
// TODO improve validation
try {
const { id, hash, source, notes } = req.body;
if (id === undefined && hash === undefined) {
res.status(400).send("Must provide 'id' or 'hash'");
return;
}
db.blockData({ id, hash, source, notes });
// TODO check return value
res.json({ message: 'Content blocked' });
} catch (error: any) {
res.status(500).send(error?.message);
}
});

// Queue a TX ID for processing
app.post('/ar-io/admin/queue-tx', express.json(), async (req, res) => {
try {
const { id } = req.body;
if (id === undefined) {
res.status(400).send("Must provide 'id'");
return;
}
prioritizedTxIds.add(id);
txFetcher.queueTxId(id);
res.json({ message: 'TX queued' });
} catch (error: any) {
res.status(500).send(error?.message);
}
});

return app;
};
};
Loading

0 comments on commit f6466c3

Please sign in to comment.