Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/x402-gateway",
"version": "0.2.0",
"version": "0.2.1",
"type": "module",
"description": "Sell crawl access to AI training crawlers by the day over x402, settled by CoinPay. One middleware: 402 with an offer, a sales page with CLI instructions, signed passes, and a robots.txt that keeps search crawlers welcome.",
"keywords": [
Expand Down
23 changes: 16 additions & 7 deletions src/edge.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,25 @@ export function inCidrs(ip, compiled) {
/**
* The caller's address, as the edge reported it.
*
* `x-forwarded-for` is a list the client can seed; the LAST hop appended by
* our own edge is trustworthy and the first is not, but every platform in
* front of these sites (Railway, a bare droplet behind nginx) puts the real
* client first and nothing else, so first is what is used. `x-real-ip` is the
* nginx spelling of the same thing.
* `x-forwarded-for` is a list the client can seed, and a denylist read from
* its first entry is a denylist any client can step around by sending one.
* The entry our own edge appends is the LAST, on every platform in front of
* these sites (Railway's proxy, nginx with `$proxy_add_x_forwarded_for`), so
* last is what is used. `x-real-ip` is nginx's spelling of the same hop and
* is preferred when present, because nginx sets it from the socket and
* nothing a client sends survives into it.
*
* A CDN in front of the edge would make the last hop the CDN's; put its
* ranges nowhere near `denyCidrs` and this still fails safe: nothing is
* refused, nothing is charged, by this check.
*/
export function clientIp(request) {
const real = request.headers.get('x-real-ip')?.trim();
if (real) return real;
const xff = request.headers.get('x-forwarded-for');
if (xff) return xff.split(',')[0].trim();
return request.headers.get('x-real-ip')?.trim() ?? '';
if (!xff) return '';
const hops = xff.split(',').map((h) => h.trim()).filter(Boolean);
return hops[hops.length - 1] ?? '';
}

/* -------------------------------------------------------------- spoofing -- */
Expand Down
11 changes: 7 additions & 4 deletions test/gateway.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ describe('the gate', () => {
describe('crawlers that do not say who they are', () => {
const OVH = ['51.38.0.0/16', '54.38.0.0/16', '141.94.0.0/16'];
const from = (ip, extra = {}) =>
req('/topics/x', { ua: CHROME, ...extra, headers: { 'x-forwarded-for': `${ip}, 10.0.0.1`, ...(extra.headers ?? {}) } });
req('/topics/x', { ua: CHROME, ...extra, headers: { 'x-forwarded-for': `10.0.0.1, ${ip}`, ...(extra.headers ?? {}) } });

it('parses CIDRs and matches addresses, and drops what it cannot read', () => {
const c = compileCidrs([...OVH, 'garbage', '1.2.3.4', '300.1.1.1/8', '10.0.0.0/33']);
Expand All @@ -362,9 +362,12 @@ describe('crawlers that do not say who they are', () => {
assert.equal(inCidrs('9.9.9.9', compileCidrs(['0.0.0.0/0'])), true);
});

it('reads the client address the way the edge writes it', () => {
assert.equal(clientIp(req('/', { headers: { 'x-forwarded-for': '203.0.113.9, 10.1.1.1' } })), '203.0.113.9');
assert.equal(clientIp(req('/', { headers: { 'x-real-ip': '203.0.113.10' } })), '203.0.113.10');
it('reads the client address from the hop our own edge appended, never one the client seeded', () => {
// A client that sends its own X-Forwarded-For puts a lie first; the edge appends the truth.
assert.equal(clientIp(req('/', { headers: { 'x-forwarded-for': '1.1.1.1, 203.0.113.9' } })), '203.0.113.9');
assert.equal(clientIp(req('/', { headers: { 'x-forwarded-for': '203.0.113.9' } })), '203.0.113.9');
assert.equal(clientIp(req('/', { headers: { 'x-real-ip': '203.0.113.10', 'x-forwarded-for': '1.1.1.1, 5.5.5.5' } })), '203.0.113.10');
assert.equal(clientIp(req('/', { headers: { 'x-forwarded-for': ' , ' } })), '');
assert.equal(clientIp(req('/')), '');
});

Expand Down
Loading