From 988da8eaf1e6db6a3f0c15c931c6b9d2afb40e3c Mon Sep 17 00:00:00 2001 From: czarny Date: Tue, 7 Oct 2025 15:52:58 +0200 Subject: [PATCH] Added handling of proxy trust. --- README.md | 14 +++++- lib/provider/aws/create-request.js | 1 + lib/provider/azure/create-request.js | 12 ++++- lib/request.js | 10 +++- package-lock.json | 2 + package.json | 1 + serverless-http.d.ts | 3 +- test/express.js | 73 ++++++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1a47857..3c2a2c4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Thank you to Upstash for reaching out to sponsor this project!
  • Price scales to zero with per request pricing
  • Built-in REST API designed for serverless and edge functions
  • - + [Start for free in 30 seconds!](https://upstash.com/?utm_source=serverless-http) @@ -53,6 +53,18 @@ Thank you to Upstash for reaching out to sponsor this project! * [Genezio](https://genezio.com/deploy-nodejs-express-on-genezio-serverless/) * Azure (Experimental, untested, probably outdated) +### Proxy trust +Serverless-http may use `x-forwarded-for` headers to provide accurate client IP addresses when your application is behind proxies or load balancers. + +Proxy trust can be enabled using the `proxyTrust (Function|Array|String)` option, see [proxy-addr](https://www.npmjs.com/package/proxy-addr) for details: + +```javascript +const handler = serverless(app, { + proxyTrust: () => true // Enable full proxy trust + proxyTrust: ['loopback', '192.168.0.0/16'] // Enable only specific address +}); +``` + ## Deploy a Hello Word on Genezio :rocket: You can deploy your own hello world example using the Express framework to Genezio with one click: diff --git a/lib/provider/aws/create-request.js b/lib/provider/aws/create-request.js index f2f76c5..410885a 100644 --- a/lib/provider/aws/create-request.js +++ b/lib/provider/aws/create-request.js @@ -98,6 +98,7 @@ module.exports = (event, context, options) => { body, remoteAddress, url, + proxyTrust: options.proxyTrust, }); req.requestContext = event.requestContext; diff --git a/lib/provider/azure/create-request.js b/lib/provider/azure/create-request.js index de6f34b..cfe3d69 100644 --- a/lib/provider/azure/create-request.js +++ b/lib/provider/azure/create-request.js @@ -25,11 +25,17 @@ function requestBody(request) { throw new Error(`Unexpected request.body type: ${typeof request.rawBody}`); } -module.exports = (request) => { +function requestRemoteAddress(event) { + return event.requestContext.identity.sourceIp; +} + + +module.exports = (request, options) => { const method = request.method; const query = request.query; const headers = requestHeaders(request); const body = requestBody(request); + const remoteAddress = requestRemoteAddress(request); const req = new Request({ method, @@ -38,7 +44,9 @@ module.exports = (request) => { url: url.format({ pathname: request.url, query - }) + }), + remoteAddress, + proxyTrust: options.proxyTrust, }); req.requestContext = request.requestContext; return req; diff --git a/lib/request.js b/lib/request.js index d1ffe76..3b69180 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,10 +1,11 @@ 'use strict'; const http = require('http'); +const proxyAddr = require('proxy-addr'); const { PassThrough } = require('stream'); module.exports = class ServerlessRequest extends http.IncomingMessage { - constructor({ method, url, headers, body, remoteAddress }) { + constructor({ method, url, headers, body, remoteAddress, proxyTrust }) { // Create a real readable socket for IncomingMessage instead of a stub. const socket = new PassThrough(); socket.encrypted = true; @@ -17,8 +18,13 @@ module.exports = class ServerlessRequest extends http.IncomingMessage { headers['content-length'] = Buffer.byteLength(body); } + Object.defineProperty(this, 'ip', { + get: () => { + return proxyAddr(this, proxyTrust || proxyAddr.compile([])); + } + }); + Object.assign(this, { - ip: remoteAddress, complete: true, httpVersion: '1.1', httpVersionMajor: '1', diff --git a/package-lock.json b/package-lock.json index 7373236..109d3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "pino": "^8.15.0", "pino-http": "^8.5.0", "polka": "^0.5.2", + "proxy-addr": "^2.0.7", "reflect-metadata": "^0.1.13", "restana": "^4.0.7", "sails": "^1.2.3", @@ -12226,6 +12227,7 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" diff --git a/package.json b/package.json index bee9431..90035cd 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "pino": "^8.15.0", "pino-http": "^8.5.0", "polka": "^0.5.2", + "proxy-addr": "^2.0.7", "reflect-metadata": "^0.1.13", "restana": "^4.0.7", "sails": "^1.2.3", diff --git a/serverless-http.d.ts b/serverless-http.d.ts index 965f8ba..aa44496 100644 --- a/serverless-http.d.ts +++ b/serverless-http.d.ts @@ -24,7 +24,8 @@ declare namespace ServerlessHttp { request?: Object | Function, response?: Object | Function, binary?: boolean | Function | string | string[], - basePath?: string + basePath?: string, + proxyTrust?: Function | Array | String } /** * AWS Lambda APIGatewayProxyHandler-like handler. diff --git a/test/express.js b/test/express.js index 300c454..9821689 100644 --- a/test/express.js +++ b/test/express.js @@ -178,6 +178,79 @@ describe('express', () => { }); }); + it('ip should come from identity source ip for aws', () => { + app.use(morgan('short')); + app.use((req, res) => { + res.status(200).send(req.ip); + }); + + return request(app, { + httpMethod: 'GET', + requestContext: { + identity: { + sourceIp: '127.0.0.1' + } + } + }) + .then(response => { + expect(response.statusCode).to.equal(200); + expect(response.body).to.equal('127.0.0.1'); + }); + }); + + it('ip should come from x-forwarded-for header if present', () => { + app.use(morgan('short')); + app.use((req, res) => { + res.status(200).send(req.ip); + }); + + return request(app, { + httpMethod: 'GET', + headers: { + 'x-forwarded-for': '1.3.3.7' + }, + requestContext: { + identity: { + sourceIp: '127.0.0.1' + } + } + }, + { + proxyTrust: () => true + }) + .then(response => { + expect(response.statusCode).to.equal(200); + expect(response.body).to.equal('1.3.3.7'); + }); + }); + + + it('ip should come from x-forwarded-for header if present 2', () => { + app.use(morgan('short')); + app.use((req, res) => { + res.status(200).send(req.ip); + }); + + return request(app, { + httpMethod: 'GET', + headers: { + 'x-forwarded-for': '192.0.0.1, 1.3.3.7' + }, + requestContext: { + identity: { + sourceIp: '127.0.0.1' + } + } + }, + { + proxyTrust: () => true + }) + .then(response => { + expect(response.statusCode).to.equal(200); + expect(response.body).to.equal('192.0.0.1'); + }); + }); + it('destroy weird', () => { app.use((req, res) => { // this was causing a .destroy is not a function error