Skip to content

Commit

Permalink
refactor(routes): Move routes into a separate file
Browse files Browse the repository at this point in the history
As routes have five clear categories, splitting them into separate files
to group them by category makes locating them in the file system
easier. Related to issue #31
  • Loading branch information
kay-is authored and djwhitt committed Oct 12, 2023
1 parent f1d2381 commit 261a901
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 160 deletions.
162 changes: 8 additions & 154 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,14 @@
*/
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 { createArnsMiddleware } from './middleware/arns.js';
import { createSandboxMiddleware } from './middleware/sandbox.js';
import {
DATA_PATH_REGEX,
RAW_DATA_PATH_REGEX,
createDataHandler,
createRawDataHandler,
} from './routes/data.js';
import { arIoRouter } from './routes/ar-io.js';
import { arnsRouter } from './routes/arns.js';
import { dataRouter } from './routes/data/index.js';
import { apolloServer } from './routes/graphql/index.js';
import { openApiRouter } from './routes/openapi.js';
import * as system from './system.js';

system.arweaveClient.refreshPeers();
Expand All @@ -48,136 +39,12 @@ if (config.START_WRITERS) {
// HTTP server
const app = express();

// TODO get path relative to source file instead of cwd
//app.use(
// OpenApiValidator.middleware({
// apiSpec: './docs/openapi.yaml',
// validateRequests: true, // (default)
// validateResponses: true, // false by default
// }),
//);

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,
});

if (config.ARNS_ROOT_HOST !== undefined) {
app.use(
createArnsMiddleware({
dataHandler,
nameResolver: system.nameResolver,
}),
);

app.use(
createSandboxMiddleware({
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);
}
});
app.use(arnsRouter);
app.use(openApiRouter);
app.use(arIoRouter);
app.use(dataRouter);

// GraphQL
const apolloServerInstanceGql = apolloServer(system.db, {
Expand All @@ -193,16 +60,3 @@ apolloServerInstanceGql.start().then(() => {
log.info(`Listening on port ${config.PORT}`);
});
});

// Data routes
app.get(
RAW_DATA_PATH_REGEX,
createRawDataHandler({
log,
dataIndex: system.contiguousDataIndex,
dataSource: system.contiguousDataSource,
blockListValidator: system.blockListValidator,
}),
);

app.get(DATA_PATH_REGEX, dataHandler);
2 changes: 1 addition & 1 deletion src/middleware/arns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Handler } from 'express';
import { asyncMiddleware } from 'middleware-async';

import * as config from '../config.js';
import { sendNotFound } from '../routes/data.js';
import { sendNotFound } from '../routes/data/handlers.js';
import { NameResolver } from '../types.js';

const EXCLUDED_SUBDOMAINS = new Set('www');
Expand Down
103 changes: 103 additions & 0 deletions src/routes/ar-io.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* 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 { Router, default as express } from 'express';
import createPrometheusMiddleware from 'express-prometheus-middleware';

import * as config from '../config.js';
import * as system from '../system.js';

export const arIoRouter = Router();

arIoRouter.use(
createPrometheusMiddleware({
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}/,
],
}),
);

// Healthcheck
arIoRouter.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
arIoRouter.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
arIoRouter.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)
arIoRouter.get('/ar-io/admin/debug', async (_req, res) => {
res.json({
db: await system.db.getDebugInfo(),
});
});

// Block access to contiguous data by ID or hash
arIoRouter.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
arIoRouter.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);
}
});
41 changes: 41 additions & 0 deletions src/routes/arns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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 { Router } from 'express';

import * as config from '../config.js';
import { createArnsMiddleware } from '../middleware/arns.js';
import { createSandboxMiddleware } from '../middleware/sandbox.js';
import * as system from '../system.js';
import { dataHandler } from './data/index.js';

export const arnsRouter = Router();

if (config.ARNS_ROOT_HOST !== undefined) {
arnsRouter.use(
createArnsMiddleware({
dataHandler,
nameResolver: system.nameResolver,
}),
);

arnsRouter.use(
createSandboxMiddleware({
sandboxProtocol: config.SANDBOX_PROTOCOL,
}),
);
}
7 changes: 2 additions & 5 deletions src/routes/data.ts → src/routes/data/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import { default as asyncHandler } from 'express-async-handler';
import url from 'node:url';
import { Logger } from 'winston';

import { MANIFEST_CONTENT_TYPE } from '../lib/encoding.js';
import { MANIFEST_CONTENT_TYPE } from '../../lib/encoding.js';
import {
BlockListValidator,
ContiguousData,
ContiguousDataAttributes,
ContiguousDataIndex,
ContiguousDataSource,
ManifestPathResolver,
} from '../types.js';
} from '../../types.js';

const STABLE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
const UNSTABLE_MAX_AGE = 60 * 60 * 2; // 2 hours
Expand Down Expand Up @@ -89,7 +89,6 @@ export const sendNotFound = (res: Response) => {
};

// Data routes
export const RAW_DATA_PATH_REGEX = /^\/raw\/([a-zA-Z0-9-_]{43})\/?$/i;
export const createRawDataHandler = ({
log,
dataIndex,
Expand Down Expand Up @@ -259,8 +258,6 @@ const sendManifestResponse = async ({
return false;
};

export const DATA_PATH_REGEX =
/^\/?([a-zA-Z0-9-_]{43})\/?$|^\/?([a-zA-Z0-9-_]{43})\/(.*)$/i;
export const createDataHandler = ({
log,
dataIndex,
Expand Down
Loading

0 comments on commit 261a901

Please sign in to comment.