Skip to content

Commit b9400c7

Browse files
committed
WIP configurable ArNS 404 page support
1 parent 79792e1 commit b9400c7

File tree

6 files changed

+134
-108
lines changed

6 files changed

+134
-108
lines changed

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

+93-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,75 @@ 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+
console.log('================================');
125+
console.log(req.headers.host);
126+
console.log(refererHost);
127+
console.log('================================');
128+
if (req.path === '/') {
129+
res.status(404);
130+
} else if (req.headers.host !== refererHost) {
131+
sendNotFound(res);
132+
return;
133+
}
134+
if (
135+
// ArNS undername should not use custom 404 pages.
136+
// TODO: expand this explanation
137+
arnsSubdomain?.match(/_/) ||
138+
// Custom 404s should not be used for Apex ArNS.
139+
hostNameIsArNSRoot
140+
) {
141+
sendNotFound(res);
142+
return;
143+
} else if (config.ARNS_NOT_FOUND_TX_ID !== undefined) {
144+
resolvedId = config.ARNS_NOT_FOUND_TX_ID;
145+
} else if (config.ARNS_NOT_FOUND_ARNS_NAME !== undefined) {
146+
({ resolvedId, ttl, processId, resolvedAt, limit, index } =
147+
await nameResolver.resolve({
148+
name: config.ARNS_NOT_FOUND_ARNS_NAME,
149+
}));
150+
} else {
151+
sendNotFound(res);
152+
return;
153+
}
91154
}
92155

93156
res.header(headerNames.arnsResolvedId, resolvedId);
94-
res.header(headerNames.arnsTtlSeconds, ttl.toString());
157+
res.header(headerNames.arnsTtlSeconds, ttl?.toString());
95158
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());
159+
res.header(headerNames.arnsResolvedAt, resolvedAt?.toString());
160+
// Limit and index can be undefined if they come from a cache that existed
161+
// before they were added.
162+
if (limit !== undefined && index !== undefined) {
163+
res.header(headerNames.arnsLimit, limit.toString());
164+
res.header(headerNames.arnsIndex, index.toString());
100165

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;
166+
// handle undername limit exceeded
167+
if (config.ARNS_RESOLVER_ENFORCE_UNDERNAME_LIMIT && index > limit) {
168+
sendPaymentRequired(
169+
res,
170+
'ArNS undername limit exceeded. Purchase additional undernames to continue.',
171+
);
172+
return;
173+
}
108174
}
109175

110176
// 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)