diff --git a/package.json b/package.json index 88fc2c6..6058fff 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/src/edge.js b/src/edge.js index bf29727..1f10154 100644 --- a/src/edge.js +++ b/src/edge.js @@ -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 -- */ diff --git a/test/gateway.test.js b/test/gateway.test.js index b78e18e..a693223 100644 --- a/test/gateway.test.js +++ b/test/gateway.test.js @@ -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']); @@ -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('/')), ''); });