Skip to content

Commit 4480355

Browse files
committed
feat(arns): add support for configurable 404 pages PE-8008
Implements customizable ArNS 404 pages that can be configured using either: - ARNS_NOT_FOUND_TX_ID: Transaction ID for custom 404 content - ARNS_NOT_FOUND_ARNS_NAME: ArNS name to resolve for 404 content Resources (css, images, etc.) referenced on custom 404 pages using relative links will return 200s as long as the referer is from the same domain. With a missing referer or a referer from another domain they will redirect to the root path so that the custom 404 page can be served. Also refactors root route handling and improves the ArNS middleware to support custom error pages while maintaining proper behavior for apex IDs and ArNS undernames.
1 parent 79792e1 commit 4480355

File tree

8 files changed

+237
-118
lines changed

8 files changed

+237
-118
lines changed

docker-compose.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ services:
142142
- GATEWAY_PEERS_REQUEST_WINDOW_COUNT=${GATEWAY_PEERS_REQUEST_WINDOW_COUNT:-}
143143
- APEX_TX_ID=${APEX_TX_ID:-}
144144
- APEX_ARNS_NAME=${APEX_ARNS_NAME:-}
145+
- ARNS_NOT_FOUND_TX_ID=${ARNS_NOT_FOUND_TX_ID:-}
146+
- ARNS_NOT_FOUND_ARNS_NAME=${ARNS_NOT_FOUND_ARNS_NAME:-}
145147
networks:
146148
- ar-io-network
147149
depends_on:

src/app.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Server } from 'node:http';
2222
import * as config from './config.js';
2323
import { headerNames } from './constants.js';
2424
import log from './log.js';
25-
import { defaultRouter } from './routes/default.js';
25+
import { rootRouter } from './routes/root.js';
2626
import { arIoRouter } from './routes/ar-io.js';
2727
import { arnsRouter } from './routes/arns.js';
2828
import { chunkRouter } from './routes/chunk/index.js';
@@ -60,7 +60,7 @@ app.use(openApiRouter);
6060
app.use(arIoRouter);
6161
app.use(chunkRouter);
6262
app.use(dataRouter);
63-
app.use(defaultRouter);
63+
app.use(rootRouter);
6464

6565
// GraphQL
6666
const apolloServerInstanceGql = apolloServer(system.gqlQueryable, {

src/config.ts

+14
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,20 @@ if (APEX_ARNS_NAME !== undefined && ARNS_ROOT_HOST === undefined) {
515515
throw new Error('ARNS_ROOT_HOST must be defined when APEX_ARNS_NAME is used');
516516
}
517517

518+
export const ARNS_NOT_FOUND_TX_ID = env.varOrUndefined('ARNS_NOT_FOUND_TX_ID');
519+
520+
export const ARNS_NOT_FOUND_ARNS_NAME = env.varOrUndefined(
521+
'ARNS_NOT_FOUND_ARNS_NAME',
522+
);
523+
if (
524+
ARNS_NOT_FOUND_TX_ID !== undefined &&
525+
ARNS_NOT_FOUND_ARNS_NAME !== undefined
526+
) {
527+
throw new Error(
528+
'ARNS_NOT_FOUND_TX_ID and ARNS_NOT_FOUND_ARNS_NAME are mutually exclusive but both are set.',
529+
);
530+
}
531+
518532
//
519533
// Header caching
520534
//

src/middleware/arns.ts

+89-27
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
import { Handler } from 'express';
1919
import { asyncMiddleware } from 'middleware-async';
20+
import { URL } from 'node:url';
2021

2122
import * as config from '../config.js';
2223
import { headerNames } from '../constants.js';
@@ -37,11 +38,35 @@ export const createArnsMiddleware = ({
3738
nameResolver: NameResolver;
3839
}): Handler =>
3940
asyncMiddleware(async (req, res, next) => {
41+
// Skip all ArNS processing if the root ArNS host is not set.
42+
if (config.ARNS_ROOT_HOST === undefined || config.ARNS_ROOT_HOST === '') {
43+
next();
44+
return;
45+
}
46+
47+
const hostNameIsArNSRoot = req.hostname === config.ARNS_ROOT_HOST;
48+
4049
if (
41-
// Ignore requests that do end with the ArNS root hostname.
42-
(config.ARNS_ROOT_HOST !== undefined &&
43-
config.ARNS_ROOT_HOST !== '' &&
44-
!req.hostname.endsWith('.' + config.ARNS_ROOT_HOST)) ||
50+
// Use apex ID as ArNS root data if it's set.
51+
config.APEX_TX_ID !== undefined &&
52+
hostNameIsArNSRoot
53+
) {
54+
res.header(headerNames.arnsResolvedId, config.APEX_TX_ID);
55+
dataHandler(req, res, next);
56+
return;
57+
}
58+
59+
let arnsSubdomain: string | undefined;
60+
if (
61+
// If apex ArNS name is set and hostname matches root use the apex ArNS
62+
// name as the ArNS subdomain.
63+
config.APEX_ARNS_NAME !== undefined &&
64+
hostNameIsArNSRoot
65+
) {
66+
arnsSubdomain = config.APEX_ARNS_NAME;
67+
} else if (
68+
// Ignore requests that do not end with the ArNS root hostname.
69+
!req.hostname.endsWith('.' + config.ARNS_ROOT_HOST) ||
4570
// Ignore requests that do not include subdomains since ArNS always
4671
// requires a subdomain.
4772
!Array.isArray(req.subdomains) ||
@@ -51,22 +76,22 @@ export const createArnsMiddleware = ({
5176
) {
5277
next();
5378
return;
79+
} else {
80+
arnsSubdomain = req.subdomains[req.subdomains.length - 1];
5481
}
5582

56-
const arnsSubdomain = req.subdomains[req.subdomains.length - 1];
57-
5883
if (
5984
EXCLUDED_SUBDOMAINS.has(arnsSubdomain) ||
60-
// Avoid collisions with sandbox URLs by ensuring the subdomain length
61-
// is below the minimum length of a sandbox subdomain. Undernames are
62-
// are an exception because they can be longer and '_' cannot appear in
63-
// base32.
85+
// Avoid collisions with sandbox URLs by ensuring the subdomain length is
86+
// below the minimum length of a sandbox subdomain. Undernames are are an
87+
// exception because they can be longer and '_' cannot appear in base32.
6488
(arnsSubdomain.length > MAX_ARNS_NAME_LENGTH && !arnsSubdomain.match(/_/))
6589
) {
6690
next();
6791
return;
6892
}
6993

94+
// TODO: add comment explaining this behavior
7095
if (DATA_PATH_REGEX.test(req.path)) {
7196
next();
7297
return;
@@ -77,34 +102,71 @@ export const createArnsMiddleware = ({
77102
return;
78103
}
79104

80-
// NOTE: Errors and request deduplication are expected to be handled by the
81-
// resolver
105+
// NOTE: Errors and in-flight resolution deduplication are expected to be
106+
// handled by the resolver.
82107
const end = metrics.arnsResolutionTime.startTimer();
83-
const { resolvedId, ttl, processId, resolvedAt, limit, index } =
108+
let { resolvedId, ttl, processId, resolvedAt, limit, index } =
84109
await nameResolver.resolve({
85110
name: arnsSubdomain,
86111
});
87112
end();
88113
if (resolvedId === undefined) {
89-
sendNotFound(res);
90-
return;
114+
// Extract host from referer if available
115+
let refererHost;
116+
if (req.headers.referer !== undefined) {
117+
try {
118+
const refererUrl = new URL(req.headers.referer);
119+
refererHost = refererUrl.host;
120+
} catch (e) {
121+
// Invalid URL, ignore
122+
}
123+
}
124+
if (req.path === '' || req.path === '/') {
125+
res.status(404);
126+
} else if (req.headers.host !== refererHost) {
127+
res.redirect('/');
128+
return;
129+
}
130+
if (
131+
// ArNS undername should not use custom 404 pages.
132+
// TODO: expand this explanation
133+
arnsSubdomain?.match(/_/) ||
134+
// Custom 404s should not be used for Apex ArNS.
135+
hostNameIsArNSRoot
136+
) {
137+
sendNotFound(res);
138+
return;
139+
} else if (config.ARNS_NOT_FOUND_TX_ID !== undefined) {
140+
resolvedId = config.ARNS_NOT_FOUND_TX_ID;
141+
} else if (config.ARNS_NOT_FOUND_ARNS_NAME !== undefined) {
142+
({ resolvedId, ttl, processId, resolvedAt, limit, index } =
143+
await nameResolver.resolve({
144+
name: config.ARNS_NOT_FOUND_ARNS_NAME,
145+
}));
146+
} else {
147+
sendNotFound(res);
148+
return;
149+
}
91150
}
92151

93152
res.header(headerNames.arnsResolvedId, resolvedId);
94-
res.header(headerNames.arnsTtlSeconds, ttl.toString());
153+
res.header(headerNames.arnsTtlSeconds, ttl?.toString());
95154
res.header(headerNames.arnsProcessId, processId);
96-
res.header(headerNames.arnsResolvedAt, resolvedAt.toString());
97-
// limit and index can be undefined if they come from the cache
98-
res.header(headerNames.arnsLimit, limit?.toString());
99-
res.header(headerNames.arnsIndex, index?.toString());
155+
res.header(headerNames.arnsResolvedAt, resolvedAt?.toString());
156+
// Limit and index can be undefined if they come from a cache that existed
157+
// before they were added.
158+
if (limit !== undefined && index !== undefined) {
159+
res.header(headerNames.arnsLimit, limit.toString());
160+
res.header(headerNames.arnsIndex, index.toString());
100161

101-
// handle undername limit exceeded
102-
if (config.ARNS_RESOLVER_ENFORCE_UNDERNAME_LIMIT && index > limit) {
103-
sendPaymentRequired(
104-
res,
105-
'ArNS undername limit exceeded. Purchase additional undernames to continue.',
106-
);
107-
return;
162+
// handle undername limit exceeded
163+
if (config.ARNS_RESOLVER_ENFORCE_UNDERNAME_LIMIT && index > limit) {
164+
sendPaymentRequired(
165+
res,
166+
'ArNS undername limit exceeded. Purchase additional undernames to continue.',
167+
);
168+
return;
169+
}
108170
}
109171

110172
// TODO: add a header for arns cache status

src/routes/arns.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ if (config.ARNS_ROOT_HOST !== undefined) {
4343
);
4444
}
4545

46+
// TODO: consider moving this into ar-io router
4647
arnsRouter.get('/ar-io/resolver/:name', async (req, res) => {
4748
const { name } = req.params;
4849
// NOTE: Errors and request deduplication are expected to be handled by the

src/routes/default.ts

-79
This file was deleted.

src/routes/root.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
19+
import { Router } from 'express';
20+
import { arIoInfoHandler } from './ar-io.js';
21+
22+
export const rootRouter = Router();
23+
24+
rootRouter.get('/', arIoInfoHandler);

0 commit comments

Comments
 (0)