From 4beb2ba711285c8ddc2620e0b59873b9cef35916 Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 5 Jun 2026 20:55:48 +0200 Subject: [PATCH 1/2] Populate request context IP from the socket peer address buildContext now sets a Remote-Addr header from req.socket.remoteAddress when neither X-Forwarded-For nor Remote-Addr is present, so the Castle SDK includes context.ip on direct and localhost requests. X-Forwarded-For continues to take precedence when present. --- app.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 6734fe7..94be84a 100644 --- a/app.js +++ b/app.js @@ -91,8 +91,23 @@ function buildApp(castle = require('./castle')) { // Build the request context (IP, headers, client id) Castle needs from a Node // request. Lists/Privacy/Events are account-level and don't need it. - const buildContext = (req) => - ContextPrepareService.call(req, {}, castle.configuration); + // + // The SDK derives the client IP from headers only (X-Forwarded-For, then + // Remote-Addr). In Node the peer address lives on the socket rather than in a + // header, so on localhost neither is present and Castle rejects the call with + // "context[ip] is missing". Expose the socket peer as Remote-Addr to mirror + // what WSGI/Rack do automatically. Behind a proxy, X-Forwarded-For (set with + // the real client IP) takes precedence and this fallback is ignored. + const buildContext = (req) => { + if ( + !req.headers['x-forwarded-for'] && + !req.headers['remote-addr'] && + req.socket?.remoteAddress + ) { + req.headers['remote-addr'] = req.socket.remoteAddress; + } + return ContextPrepareService.call(req, {}, castle.configuration); + }; // a default value reused across the login / password-reset demos let registeredAt = '2020-02-23T22:28:55.387Z'; From b26ec112396a77f0715814d822071fb60a91d6ab Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 5 Jun 2026 21:17:15 +0200 Subject: [PATCH 2/2] Limit the socket IP fallback to loopback connections The Remote-Addr fallback now applies only when the connection's peer address is loopback (local development). Requests carrying X-Forwarded-For or Remote-Addr, and non-loopback connections, are left untouched. --- app.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 94be84a..bba9e62 100644 --- a/app.js +++ b/app.js @@ -47,6 +47,12 @@ function errorResult(err) { return { error: err instanceof APIError ? err.message : String(err) }; } +// True for IPv4/IPv6 loopback addresses, including the IPv4-mapped IPv6 form +// Node reports for localhost connections. +function isLoopback(ip) { + return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1'; +} + // Build the Express app around a Castle client. Accepting the client as an // argument keeps the routes easy to test (the SDK can be stubbed). function buildApp(castle = require('./castle')) { @@ -94,17 +100,20 @@ function buildApp(castle = require('./castle')) { // // The SDK derives the client IP from headers only (X-Forwarded-For, then // Remote-Addr). In Node the peer address lives on the socket rather than in a - // header, so on localhost neither is present and Castle rejects the call with - // "context[ip] is missing". Expose the socket peer as Remote-Addr to mirror - // what WSGI/Rack do automatically. Behind a proxy, X-Forwarded-For (set with - // the real client IP) takes precedence and this fallback is ignored. + // header, so running this demo directly on localhost neither is present and + // Castle rejects the call with "context[ip] is missing". When the connection + // is loopback (i.e. local development) expose the socket peer as Remote-Addr + // so the demo works. In production a proxy sets X-Forwarded-For with the real + // client IP, so this fallback never applies. const buildContext = (req) => { + const peer = req.socket?.remoteAddress; if ( + peer && + isLoopback(peer) && !req.headers['x-forwarded-for'] && - !req.headers['remote-addr'] && - req.socket?.remoteAddress + !req.headers['remote-addr'] ) { - req.headers['remote-addr'] = req.socket.remoteAddress; + req.headers['remote-addr'] = peer; } return ContextPrepareService.call(req, {}, castle.configuration); };