diff --git a/docker-compose.yaml b/docker-compose.yaml index 8849f52a..327aef82 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,3 +44,5 @@ services: - ADMIN_API_KEY=${ADMIN_API_KEY:-} - ANS104_UNBUNDLE_FILTER=${ANS104_UNBUNDLE_FILTER:-} - ANS104_DATA_INDEX_FILTER=${ANS104_DATA_INDEX_FILTER:-} + - ARNS_ROOT_HOST=${ARNS_ROOT_HOST:-} + - SANDBOX_PROTOCOL=${SANDBOX_PROTOCOL:-} diff --git a/package.json b/package.json index 403900a7..7ed2cc7f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "prom-client": "^14.0.1", "ramda": "^0.28.0", "retry-axios": "^3.0.0", + "rfc4648": "^1.5.2", "sql-bricks": "^3.0.0", "sql-bricks-sqlite": "^0.1.0", "stream-chain": "^2.2.5", diff --git a/src/app.ts b/src/app.ts index aa7ffdbf..bdffaf99 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,6 +26,7 @@ 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, @@ -80,6 +81,13 @@ app.use( }), ); +app.use( + createSandboxMiddleware({ + rootHost: config.ARNS_ROOT_HOST, + sandboxProtocol: config.SANDBOX_PROTOCOL, + }), +); + // OpenAPI Spec const openapiDocument = YAML.parse( fs.readFileSync('docs/openapi.yaml', 'utf8'), diff --git a/src/config.ts b/src/config.ts index d3285e70..ca9962dc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -55,3 +55,5 @@ export const ANS104_UNBUNDLE_FILTER = createFilter( export const ANS104_DATA_INDEX_FILTER = createFilter( JSON.parse(env.varOrDefault('ANS104_DATA_INDEX_FILTER', '{"never": true}')), ); +export const ARNS_ROOT_HOST = env.varOrUndefined('ARNS_ROOT_HOST'); +export const SANDBOX_PROTOCOL = env.varOrUndefined('SANDBOX_PROTOCOL'); diff --git a/src/middleware/sandbox.ts b/src/middleware/sandbox.ts new file mode 100644 index 00000000..e8e85ce2 --- /dev/null +++ b/src/middleware/sandbox.ts @@ -0,0 +1,72 @@ +/** + * AR.IO Gateway + * Copyright (C) 2022 - 2023 Permanent Data Solutions, Inc + * + * 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 { Handler, Request } from 'express'; +import url from 'node:url'; +import { base32 } from 'rfc4648'; + +import { fromB64Url } from '../lib/encoding.js'; + +function getRequestSandbox(req: Request): string | undefined { + if (req.subdomains.length === 1) { + return req.subdomains[0]; + } + return undefined; +} + +function getRequestId(req: Request): string | undefined { + return (req.path.match(/^\/([a-zA-Z0-9-_]{43})/) ?? [])[1]; +} + +function sandboxFromId(id: string): string { + return base32.stringify(fromB64Url(id), { pad: false }).toLowerCase(); +} + +export function createSandboxMiddleware({ + rootHost, + sandboxProtocol, +}: { + rootHost?: string; + sandboxProtocol?: string; +}): Handler { + return (req, res, next) => { + if (rootHost === undefined) { + next(); + return; + } + + const id = getRequestId(req); + if (id === undefined) { + next(); + return; + } + + const reqSandbox = getRequestSandbox(req); + const idSandbox = sandboxFromId(id); + if (reqSandbox !== idSandbox) { + const queryString = url.parse(req.originalUrl).query ?? ''; + const path = req.path.replace(/\/\//, '/'); + const protocol = sandboxProtocol ?? (req.secure ? 'https' : 'http'); + return res.redirect( + 302, + `${protocol}://${idSandbox}.${rootHost}${path}?${queryString}`, + ); + } + + next(); + }; +} diff --git a/yarn.lock b/yarn.lock index c629f4cd..76fd0a9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5275,6 +5275,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc4648@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.2.tgz#cf5dac417dd83e7f4debf52e3797a723c1373383" + integrity sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg== + rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"