diff --git a/src/app.ts b/src/app.ts index 6df5b94a..b6da322a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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(); @@ -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, { @@ -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); diff --git a/src/middleware/arns.ts b/src/middleware/arns.ts index e51d4476..2c14c48c 100644 --- a/src/middleware/arns.ts +++ b/src/middleware/arns.ts @@ -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'); diff --git a/src/routes/ar-io.ts b/src/routes/ar-io.ts new file mode 100644 index 00000000..c420f7df --- /dev/null +++ b/src/routes/ar-io.ts @@ -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 . + */ +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); + } +}); diff --git a/src/routes/arns.ts b/src/routes/arns.ts new file mode 100644 index 00000000..9b4423e9 --- /dev/null +++ b/src/routes/arns.ts @@ -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 . + */ +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, + }), + ); +} diff --git a/src/routes/data.ts b/src/routes/data/handlers.ts similarity index 97% rename from src/routes/data.ts rename to src/routes/data/handlers.ts index 79d5252e..f75a96cf 100644 --- a/src/routes/data.ts +++ b/src/routes/data/handlers.ts @@ -20,7 +20,7 @@ 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, @@ -28,7 +28,7 @@ import { 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 @@ -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, @@ -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, diff --git a/src/routes/data/index.ts b/src/routes/data/index.ts new file mode 100644 index 00000000..a694fabf --- /dev/null +++ b/src/routes/data/index.ts @@ -0,0 +1,47 @@ +/** + * 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 . + */ +import { Router } from 'express'; + +import log from '../../log.js'; +import * as system from '../../system.js'; +import { createDataHandler, createRawDataHandler } from './handlers.js'; + +const DATA_PATH_REGEX = + /^\/?([a-zA-Z0-9-_]{43})\/?$|^\/?([a-zA-Z0-9-_]{43})\/(.*)$/i; +const RAW_DATA_PATH_REGEX = /^\/raw\/([a-zA-Z0-9-_]{43})\/?$/i; + +// Used by ArNS Router +export const dataHandler = createDataHandler({ + log, + dataIndex: system.contiguousDataIndex, + dataSource: system.contiguousDataSource, + blockListValidator: system.blockListValidator, + manifestPathResolver: system.manifestPathResolver, +}); + +export const dataRouter = Router(); +dataRouter.get(DATA_PATH_REGEX, dataHandler); +dataRouter.get( + RAW_DATA_PATH_REGEX, + createRawDataHandler({ + log, + dataIndex: system.contiguousDataIndex, + dataSource: system.contiguousDataSource, + blockListValidator: system.blockListValidator, + }), +); diff --git a/src/routes/openapi.ts b/src/routes/openapi.ts new file mode 100644 index 00000000..ffb06b03 --- /dev/null +++ b/src/routes/openapi.ts @@ -0,0 +1,55 @@ +/** + * 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 . + */ +//import * as OpenApiValidator from 'express-openapi-validator'; +import { Router } from 'express'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import swaggerUi from 'swagger-ui-express'; +import YAML from 'yaml'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const openApiRouter = Router(); + +// 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 +// }), +//); + +// OpenAPI Spec +const openapiDocument = YAML.parse( + fs.readFileSync(__dirname + '/../../docs/openapi.yaml', 'utf8'), +); +openApiRouter.get('/openapi.json', (_req, res) => { + res.json(openapiDocument); +}); + +// Swagger UI +const options = { + explorer: true, +}; +openApiRouter.use( + '/api-docs', + swaggerUi.serve, + swaggerUi.setup(openapiDocument, options), +);