diff --git a/.travis.yml b/.travis.yml index 2b2f440..5762380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,9 @@ language: node_js node_js: - - "0.6" - - "0.8" - - "0.10" - - "0.12" - - "1.0" - - "1.8" - - "2.0" - - "2.1" + - '1.0' + - '1.8' + - '2.0' + - '2.1' sudo: false before_install: # Setup Node.js version-specific dependencies diff --git a/LICENSE b/LICENSE index b7dce6c..714f163 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2014 Douglas Christopher Wilson +Copyright (c) 2014 Douglas Christopher Wilson, Ahmad Nassri Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index d71a3ce..1d20f74 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ [![Build Status][travis-image]][travis-url] [![Test Coverage][coveralls-image]][coveralls-url] -Parse HTTP X-Forwarded-For header +Parse *Forwarded* HTTP headers, using the standard: [RFC 7239](https://tools.ietf.org/html/rfc7239) *(Forwarded HTTP Extension)*, as well as commonly used none-standard headers (e.g. `X-Forwarded-*`, `X-Real-*`, etc ...) + +review [`schemas` folder](lib/schemas) for a full list of supported headers schemas. ## Installation @@ -20,16 +22,51 @@ $ npm install forwarded var forwarded = require('forwarded') ``` -### forwarded(req) +### forwarded(req[, options]) + +returns an object who's properties represent [RFC 7239 Parameters (Section 5)](http://tools.ietf.org/html/rfc7239#section-5) ```js -var addresses = forwarded(req) +var result = forwarded(req) ``` -Parse the `X-Forwarded-For` header from the request. Returns an array -of the addresses, including the socket address for the `req`. In reverse -order (i.e. index `0` is the socket address and the last index is the -furthest address, typically the end-user). +#### options + +| name | type | description | required | default | +| --------- | ------- | ----------------------------------------- | -------- | ---------------------------- | +| `schemas` | `array` | ordered list of header schemas to process | no | `['rfc7239', 'x-forwarded']` | + +Parse appropriate headers from the request matching the selected [schemas](#options). + +###### available schemas + +| name | key | description | +| ------------------- | ------------- | ----------------------------------------------------------------------------------------- | +| RFC 7239 | `rfc7239` | [RFC 7239 Standard][rfc7239] | +| X-Forwarded-* | `x-forwarded` | Headers using the prefix [`X-Forwarded-*`][x-forwarded], a de facto standard | +| X-Real-* | `x-real` | Headers using the prefix [`X-Real-*`][x-real], mostly common in NGINX servers | +| Z-Forwarded-* | `z-forwarded` | less common version of `X-Forwarded-*` used by [Z Scaler][z-forwarded] | +| X-Cluster-Client-IP | `x-cluster` | used by [Rackspace][x-cluster], X-Ray servers | +| Cloudflare | `cloudflare` | Headers used by [Cloudflare][cloudflare] | +| Fastly | `fastly` | Headers used by Fastly, for [IP][fastly-ip], Port, and [SSL][fastly-ssl] info | +| Microsoft | `microsoft` | Non-standard header field used by [Microsoft][microsoft] applications and load-balancers | +| Weblogic | `weblogic` | Forwarded IP by Oracle's [Weblogic][weblogic] Proxy | + +### returned object + +| name | type | description | default | +| --------- | --------- | ---------------------------------------------------------------------------------------- | -------------------------------------- | +| `for` | `array` | alias of `addrs` | +| `by` | `string` | [RFC 7239 Section 5.1](http://tools.ietf.org/html/rfc7239#section-5.1) compatible result | `null` | +| `addrs` | `array` | [RFC 7239 Section 5.2](http://tools.ietf.org/html/rfc7239#section-5.2) compatible result | `[request.connection.remoteAddress]` | +| `host` | `string` | [RFC 7239 Section 5.3](http://tools.ietf.org/html/rfc7239#section-5.3) compatible result | `request.headers.host` | +| `proto` | `string` | [RFC 7239 Section 5.4](http://tools.ietf.org/html/rfc7239#section-5.4) compatible result | `request.connection.encrypted` | +| `port` | `string` | the last known port used by the client/proxy in chain of proxies | `request.connection.remotePort` | +| `ports` | `array` | ordered list of known ports in the chain of proxies | `[request.connection.remotePort]` | + +###### Notes + +- `forwarded().addrs` & `forwarded().ports`: return arrays of the addresses & ports respectively, including the socket address/port for the request. In reverse order (i.e. index `0` is the socket address/port and the last index is the furthest address/port, typically the end-user). ## Testing @@ -37,6 +74,10 @@ furthest address, typically the end-user). $ npm test ``` +## TODO + +- [ ] extract ports from [`Forwarded`](http://tools.ietf.org/html/rfc7239#section-5.2) header: `Forwarded: for=x.x.x.x:yyyy` + ## License [MIT](LICENSE) @@ -51,3 +92,13 @@ $ npm test [coveralls-url]: https://coveralls.io/r/jshttp/forwarded?branch=master [downloads-image]: https://img.shields.io/npm/dm/forwarded.svg [downloads-url]: https://npmjs.org/package/forwarded +[rfc7239]: https://tools.ietf.org/html/rfc7239 +[x-forwarded]: https://en.wikipedia.org/wiki/X-Forwarded-For +[z-forwarded]: https://en.wikipedia.org/wiki/X-Forwarded-For#Proxy_servers_and_caching_engines +[x-real]: http://nginx.org/en/docs/http/ngx_http_realip_module.html +[x-cluster]: https://support.rackspace.com/how-to/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address/ +[cloudflare]: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-CloudFlare-handle-HTTP-Request-headers- +[fastly-ssl]: https://docs.fastly.com/guides/securing-communications/tls-termination +[fastly-ip]: https://docs.fastly.com/guides/basic-configuration/adding-or-modifying-headers-on-http-requests-and-responses +[weblogic]: https://blogs.oracle.com/wlscoherence/entry/obtaining_the_correct_client_ip +[microsoft]: http://technet.microsoft.com/en-us/library/aa997519(v=exchg.65).aspx diff --git a/index.js b/index.js index 234f0f7..17057ea 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,9 @@ /*! * forwarded * Copyright(c) 2014 Douglas Christopher Wilson + * Copyright(c) 2015 Ahmad Nassri * MIT Licensed - */ +*/ 'use strict' @@ -11,29 +12,83 @@ * @public */ -module.exports = forwarded +var processor = require('./lib/processor') +var schemas = require('./lib/schemas') /** * Get all addresses in the request, using the `X-Forwarded-For` header. * - * @param {object} req - * @return {array} + * @param {http.IncomingMessage} req + * @return {object} * @public */ -function forwarded(req) { +module.exports = function forwarded (req, options) { + options = options || {} + + var opts = { + // default to only standard and x-forwarded for backward compatibility + schemas: options.schemas || [ + 'rfc7239', + 'x-forwarded' + ] + } + if (!req) { throw new TypeError('argument req is required') } - // simple header parsing - var proxyAddrs = (req.headers['x-forwarded-for'] || '') - .split(/ *, */) - .filter(Boolean) - .reverse() - var socketAddr = req.connection.remoteAddress - var addrs = [socketAddr].concat(proxyAddrs) + // start with default values from socket connection + var forwarded = { + addrs: [req.connection.remoteAddress], + by: null, + host: req.headers && req.headers.host ? req.headers.host : undefined, + port: req.connection.remotePort ? req.connection.remotePort.toString() : undefined, + ports: [], + proto: req.connection.encrypted ? 'https' : 'http' + } + + // add default port to ports array if present + if (forwarded.port) { + forwarded.ports.push(forwarded.port) + } + + // alias "for" to keep with RFC7239 naming + forwarded.for = forwarded.addrs + + return opts.schemas + // check if schemas exist + .map(function (name) { + // adjust case + name = name.toLowerCase() + + if (!schemas[name]) { + throw new Error('invalid schema') + } + + return schemas[name] + }) + + // process schemas + .reduce(function (forwarded, schema) { + var result = processor(req.headers, schema) + + // ensure reverse order of addresses + if (typeof result.addrs === 'string') { + result.addrs = result.addrs + .split(/ *, */) + .filter(Boolean) + .reverse() + } - // return all addresses - return addrs + // update forwarded object + return { + addrs: forwarded.addrs.concat(result.addrs).filter(Boolean), + by: result.by ? result.by : forwarded.by, + host: result.host ? result.host : forwarded.host, + port: result.port ? result.port : forwarded.port, + ports: forwarded.ports.concat([result.port]).filter(Boolean), + proto: result.proto ? result.proto : forwarded.proto + } + }, forwarded) } diff --git a/lib/processor.js b/lib/processor.js new file mode 100644 index 0000000..830de47 --- /dev/null +++ b/lib/processor.js @@ -0,0 +1,30 @@ +'use strict' + +var debug = require('debug-log')('forwarded') + +module.exports = function processor (headers, schema) { + if (typeof schema === 'function') { + return schema(headers) + } + + var result = {} + + var fields = ['addrs', 'host', 'port'] + + fields.forEach(function (field) { + if (schema[field] && headers[schema[field]]) { + var value = headers[schema[field]] + + debug('found header [%s = %s]', schema[field], value) + + result[field] = value + } + }) + + // get protocol + if (typeof schema.proto === 'function') { + result.proto = schema.proto(headers) + } + + return result +} diff --git a/lib/schemas/cloudflare.js b/lib/schemas/cloudflare.js new file mode 100644 index 0000000..f5ee50d --- /dev/null +++ b/lib/schemas/cloudflare.js @@ -0,0 +1,17 @@ +'use strict' + +var debug = require('debug-log')('forwarded') + +module.exports = { + addrs: 'cf-connecting-ip', + proto: function protocol (headers) { + try { + var cf = JSON.parse(headers['cf-visitor']) + if (cf.scheme) { + return cf.scheme + } + } catch (e) { + debug('could not parse "cf-visitor" header: %s', headers['cf-visitor']) + } + } +} diff --git a/lib/schemas/fastly.js b/lib/schemas/fastly.js new file mode 100644 index 0000000..ef3eb35 --- /dev/null +++ b/lib/schemas/fastly.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = { + addrs: 'fastly-client-ip', + port: 'fastly-client-port', + proto: function protocol (headers) { + return headers['fastly-ssl'] ? 'https' : undefined + } +} diff --git a/lib/schemas/index.js b/lib/schemas/index.js new file mode 100644 index 0000000..9885322 --- /dev/null +++ b/lib/schemas/index.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports = { + 'cloudflare': require('./cloudflare'), + 'fastly': require('./fastly'), + 'microsoft': require('./microsoft'), + 'rfc7239': require('./rfc7239'), + 'weblogic': require('./weblogic'), + 'x-cluster': require('./x-cluster'), + 'x-forwarded': require('./x-forwarded'), + 'x-real': require('./x-real'), + 'z-forwarded': require('./z-forwarded') +} diff --git a/lib/schemas/microsoft.js b/lib/schemas/microsoft.js new file mode 100644 index 0000000..2663355 --- /dev/null +++ b/lib/schemas/microsoft.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = { + proto: function protocol (headers) { + if (headers['front-end-https']) { + return headers['front-end-https'] === 'on' ? 'https' : 'http' + } + } +} diff --git a/lib/schemas/rfc7239.js b/lib/schemas/rfc7239.js new file mode 100644 index 0000000..ee5c728 --- /dev/null +++ b/lib/schemas/rfc7239.js @@ -0,0 +1,27 @@ +'use strict' + +var parse = require('forwarded-parse') +var debug = require('debug-log')('forwarded') + +module.exports = function (headers) { + if (!headers.forwarded) { + return {} + } + + try { + var result = parse(headers.forwarded) + + var forwarded = { + addrs: result.for ? result.for.reverse() : [], + by: result.by ? result.by[0] : undefined, + host: result.host ? result.host[0] : undefined, + port: result.port ? result.port[0] : undefined, + ports: result.port, + proto: result.proto ? result.proto[0] : undefined + } + } catch (e) { + debug(e) + } + + return forwarded +} diff --git a/lib/schemas/weblogic.js b/lib/schemas/weblogic.js new file mode 100644 index 0000000..c57d58d --- /dev/null +++ b/lib/schemas/weblogic.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + addrs: 'wl-proxy-client-ip' +} diff --git a/lib/schemas/x-cluster.js b/lib/schemas/x-cluster.js new file mode 100644 index 0000000..99ee065 --- /dev/null +++ b/lib/schemas/x-cluster.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + addrs: 'x-cluster-client-ip' +} diff --git a/lib/schemas/x-forwarded.js b/lib/schemas/x-forwarded.js new file mode 100644 index 0000000..70daa9c --- /dev/null +++ b/lib/schemas/x-forwarded.js @@ -0,0 +1,20 @@ +'use strict' + +module.exports = { + addrs: 'x-forwarded-for', + host: 'x-forwarded-host', + port: 'x-forwarded-port', + proto: function protocol (headers) { + if (headers['x-forwarded-proto']) { + return headers['x-forwarded-proto'] + } + + if (headers['x-forwarded-protocol']) { + return headers['x-forwarded-protocol'] + } + + if (headers['x-forwarded-ssl']) { + return headers['x-forwarded-ssl'] === 'on' ? 'https' : 'http' + } + } +} diff --git a/lib/schemas/x-real.js b/lib/schemas/x-real.js new file mode 100644 index 0000000..b2c3f8c --- /dev/null +++ b/lib/schemas/x-real.js @@ -0,0 +1,15 @@ +'use strict' + +module.exports = { + addrs: 'x-real-ip', + port: 'x-real-port', + proto: function protocol (headers) { + if (headers['x-real-proto']) { + return headers['x-real-proto'] + } + + if (headers['x-url-scheme']) { + return headers['x-url-scheme'] + } + } +} diff --git a/lib/schemas/z-forwarded.js b/lib/schemas/z-forwarded.js new file mode 100644 index 0000000..5805cf8 --- /dev/null +++ b/lib/schemas/z-forwarded.js @@ -0,0 +1,16 @@ +'use strict' + +module.exports = { + addrs: 'z-forwarded-for', + host: 'z-forwarded-host', + port: 'z-forwarded-port', + proto: function protocol (headers) { + if (headers['x-forwarded-proto']) { + return headers['x-forwarded-proto'] + } + + if (headers['x-forwarded-protocol']) { + return headers['x-forwarded-protocol'] + } + } +} diff --git a/package.json b/package.json index 9c781ec..5e74349 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,62 @@ { "name": "forwarded", "description": "Parse HTTP X-Forwarded-For header", - "version": "0.1.0", + "version": "1.0.0-rc.1", "contributors": [ + "Ahmad Nassri (https://www.ahmadnassri.com)", "Douglas Christopher Wilson " ], "license": "MIT", "keywords": [ - "x-forwarded-for", + "cf-connecting-ip", + "cf-visitor", + "fastly-client-ip", + "fastly-client-port", + "fastly-ssl", + "forwarded", + "front-end-https", "http", - "req" + "https", + "protocol", + "x-cluster-client-ip", + "x-forwarded-for", + "x-forwarded-host", + "x-forwarded-port", + "x-forwarded-proto", + "x-forwarded-protocol", + "x-forwarded-ssl", + "x-real-ip", + "x-real-port", + "x-real-proto", + "x-url-scheme", + "z-forwarded-for", + "z-forwarded-host", + "z-forwarded-port", + "z-forwarded-proto", + "z-forwarded-protocol" ], "repository": "jshttp/forwarded", + "dependencies": { + "debug-log": "^1.0.0", + "forwarded-parse": "^2.0.0" + }, "devDependencies": { - "istanbul": "0.3.9", - "mocha": "1.21.5" + "istanbul": "0.4.5", + "mocha": "3.2.0" }, "files": [ "LICENSE", "HISTORY.md", "README.md", - "index.js" + "index.js", + "lib" ], "engines": { - "node": ">= 0.6" + "node": ">= 4" }, "scripts": { - "test": "mocha --reporter spec --bail --check-leaks test/", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", - "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" + "test": "mocha --recursive --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --recursive --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --recursive --reporter spec --check-leaks test/" } } diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..0473de1 --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,53 @@ +'use strict' + +var forwarded = require('..') +var http = require('http') + +var createServer = function (done) { + var server = http.createServer(function (req, res) { + res.write(JSON.stringify(forwarded(req))) + res.end() + }) + + server.listen(0, 'localhost', function () { + done(server) + }) +} + +exports.createRequestServer = function (headers, done) { + createServer(function (server) { + var options = { + headers: headers, + host: server.address().address, + port: server.address().port + } + + http.get(options, function (res) { + var body = '' + + res.on('data', function (chunk) { + body += chunk + }) + + res.on('end', function () { + server.close() + + server.on('close', function () { + done(JSON.parse(body)) + }) + }) + }) + }) +} + +exports.createRequestMock = function (headers, connection) { + return { + connection: connection || { + encrypted: true, + remoteAddress: '127.0.0.1', + remotePort: 5000 + }, + + headers: headers || {} + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..7da7cf2 --- /dev/null +++ b/test/index.js @@ -0,0 +1,84 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('..') +var request = require('./helpers').createRequestMock + +describe('forwarded(req)', function () { + it('should require http.IncomingMessage', function () { + assert.throws(forwarded.bind(null), TypeError) + }) + + it('should fail with invalid schemas', function () { + var options = { + schemas: [ + 'XFFFF' + ] + } + + assert.throws(function () { forwarded(request(), options) }, Error) + }) + + it('should not throw an error on a disconnected socket connection', function () { + var req = request({}, {remoteAddress: '127.0.0.1'}) + + assert.doesNotThrow(function () { forwarded(req) }, Error) + }) + + it('should convert schema names to lowercase', function () { + var options = { schemas: ['x-forwarded'] } + + var req = request({'x-forwarded-for': '10.0.0.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.0.0.1']) + }) + + it('should get defaults', function () { + var req = request({host: 'mockbin.com'}) + var result = forwarded(req) + + assert.equal(result.host, 'mockbin.com') + assert.equal(result.port, '5000') + assert.deepEqual(result.addrs, ['127.0.0.1']) + assert.deepEqual(result.proto, 'https') + }) + + it('should set proto=http when connection.encrypted=false', function () { + var req = request({}, { encrypted: false, remotePort: 5000 }) + var result = forwarded(req) + + assert.deepEqual(result.proto, 'http') + }) + + it('should process all schemas in ordered sequence', function () { + var options = { + schemas: [ + 'cloudflare', + 'fastly', + 'microsoft', + 'x-real', + 'x-cluster', + 'x-forwarded', + 'z-forwarded', + 'rfc7239' + ] + } + + var req = request({ + 'forwarded': 'for=0.0.0.7', + 'cf-connecting-ip': '0.0.0.1', + 'fastly-client-ip': '0.0.0.2', + 'x-real-ip': '0.0.0.3', + 'x-cluster-client-ip': '0.0.0.4', + 'x-forwarded-for': '0.0.0.5', + 'z-forwarded-for': '0.0.0.6' + }) + + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '0.0.0.1', '0.0.0.2', '0.0.0.3', '0.0.0.4', '0.0.0.5', '0.0.0.6', '0.0.0.7']) + }) +}) diff --git a/test/schemas/cloudflare.js b/test/schemas/cloudflare.js new file mode 100644 index 0000000..676469d --- /dev/null +++ b/test/schemas/cloudflare.js @@ -0,0 +1,49 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('../..') +var request = require('../helpers').createRequestMock +var schemas = require('../../lib/schemas') + +var options = { + schemas: ['cloudflare'] +} + +describe('cloudflare', function () { + it('should parse [cf-connecting-ip]', function () { + var req = request({'cf-connecting-ip': '10.10.10.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.10.10.1']) + }) + + it('should parse [cf-visitor = {"scheme": "https"}]', function () { + var req = request({'cf-visitor': '{"scheme": "https"}'}) + var result = schemas.cloudflare.proto(req.headers) + + assert.equal(result, 'https') + }) + + it('should parse [cf-visitor = {"scheme": "http"}]', function () { + var req = request({'cf-visitor': '{"scheme": "http"}'}) + var result = schemas.cloudflare.proto(req.headers) + + assert.equal(result, 'http') + }) + + it('should parse [cf-visitor = {}]', function () { + var req = request({'cf-visitor': '{}'}) + var result = schemas.cloudflare.proto(req.headers) + + assert.equal(result, undefined) + }) + + it('should parse [cf-visitor = {malformed}]', function () { + var req = request({'cf-visitor': '{malformed}'}) + var result = schemas.cloudflare.proto(req.headers) + + assert.equal(result, undefined) + }) +}) diff --git a/test/schemas/fastly.js b/test/schemas/fastly.js new file mode 100644 index 0000000..623f4e1 --- /dev/null +++ b/test/schemas/fastly.js @@ -0,0 +1,42 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('../..') +var request = require('../helpers').createRequestMock +var schemas = require('../../lib/schemas') + +var options = { + schemas: ['fastly'] +} + +describe('fastly', function () { + it('should parse [fastly-client-ip]', function () { + var req = request({'fastly-client-ip': '10.10.10.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.10.10.1']) + }) + + it('should parse [fastly-client-port]', function () { + var req = request({'fastly-client-port': '9000'}) + var result = forwarded(req, options) + + assert.deepEqual(result.port, '9000') + }) + + it('should parse [fastly-ssl = 1]', function () { + var req = request({'fastly-ssl': '1'}) + var result = schemas.fastly.proto(req.headers) + + assert.equal(result, 'https') + }) + + it('should parse [fastly-ssl = undefined]', function () { + var req = request() + var result = schemas.fastly.proto(req.headers) + + assert.equal(result, undefined) + }) +}) diff --git a/test/schemas/microsoft.js b/test/schemas/microsoft.js new file mode 100644 index 0000000..011a532 --- /dev/null +++ b/test/schemas/microsoft.js @@ -0,0 +1,16 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var request = require('../helpers').createRequestMock +var schemas = require('../../lib/schemas') + +describe('microsoft', function () { + it('should detect https protocol', function () { + var req = request({'front-end-https': 'on'}) + var result = schemas.microsoft.proto(req.headers) + + assert.equal(result, 'https') + }) +}) diff --git a/test/schemas/rfc7239.js b/test/schemas/rfc7239.js new file mode 100644 index 0000000..194a9ac --- /dev/null +++ b/test/schemas/rfc7239.js @@ -0,0 +1,30 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var request = require('../helpers').createRequestMock +var schemas = require('../../lib/schemas') + +describe('rfc7239', function () { + it('should return empty object', function () { + var req = request() + var result = schemas.rfc7239(req.headers) + + assert.deepEqual(result, {}) + }) + + it('should parse and process Forwarded header', function () { + var req = request({'forwarded': 'host=mockbin.com, for=0.0.0.1, for=0.0.0.2, for=private, for="1::8", port=9000; proto=https; by=0.0.0.3'}) + var result = schemas.rfc7239(req.headers) + + assert.deepEqual(result, { + addrs: ['1::8', 'private', '0.0.0.2', '0.0.0.1'], + by: '0.0.0.3', + port: '9000', + ports: ['9000'], + host: 'mockbin.com', + proto: 'https' + }) + }) +}) diff --git a/test/schemas/x-cluster.js b/test/schemas/x-cluster.js new file mode 100644 index 0000000..640c487 --- /dev/null +++ b/test/schemas/x-cluster.js @@ -0,0 +1,20 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('../..') +var request = require('../helpers').createRequestMock + +var options = { + schemas: ['x-cluster'] +} + +describe('x-cluster', function () { + it('should parse [x-cluster-client-ip]', function () { + var req = request({'x-cluster-client-ip': '10.10.10.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.10.10.1']) + }) +}) diff --git a/test/schemas/x-forwarded.js b/test/schemas/x-forwarded.js new file mode 100644 index 0000000..9d00af5 --- /dev/null +++ b/test/schemas/x-forwarded.js @@ -0,0 +1,55 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('../..') +var request = require('../helpers').createRequestMock + +var options = { + schemas: ['x-forwarded'] +} + +describe('x-forwarded', function () { + it('should parse [x-forwarded-for]', function () { + var req = request({'x-forwarded-for': '10.10.10.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.10.10.1']) + }) + + it('should parse [x-forwarded-host]', function () { + var req = request({'x-forwarded-host': 'mockbin.com'}) + var result = forwarded(req, options) + + assert.deepEqual(result.host, 'mockbin.com') + }) + + it('should parse [x-forwarded-port]', function () { + var req = request({'x-forwarded-port': '9000'}) + var result = forwarded(req, options) + + assert.deepEqual(result.port, '9000') + }) + + it('should parse [x-forwarded-proto]', function () { + var req = request({'x-forwarded-proto': 'https'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) + + it('should parse [x-forwarded-protocol]', function () { + var req = request({'x-forwarded-protocol': 'https'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) + + it('should parse [x-forwarded-ssl]', function () { + var req = request({'x-forwarded-ssl': 'on'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) +}) diff --git a/test/schemas/x-real.js b/test/schemas/x-real.js new file mode 100644 index 0000000..b88fb9b --- /dev/null +++ b/test/schemas/x-real.js @@ -0,0 +1,41 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('../..') +var request = require('../helpers').createRequestMock + +var options = { + schemas: ['x-real'] +} + +describe('x-real', function () { + it('should parse [x-real-ip]', function () { + var req = request({'x-real-ip': '10.10.10.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.10.10.1']) + }) + + it('should parse [x-real-port]', function () { + var req = request({'x-real-port': '9000'}) + var result = forwarded(req, options) + + assert.deepEqual(result.port, '9000') + }) + + it('should parse [x-real-proto]', function () { + var req = request({'x-real-proto': 'https'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) + + it('should parse [x-url-scheme]', function () { + var req = request({'x-url-scheme': 'https'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) +}) diff --git a/test/schemas/z-forwarded.js b/test/schemas/z-forwarded.js new file mode 100644 index 0000000..dd838a1 --- /dev/null +++ b/test/schemas/z-forwarded.js @@ -0,0 +1,48 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var forwarded = require('../..') +var request = require('../helpers').createRequestMock + +var options = { + schemas: ['z-forwarded'] +} + +describe('z-forwarded', function () { + it('should parse [z-forwarded-for]', function () { + var req = request({'z-forwarded-for': '10.10.10.1'}) + var result = forwarded(req, options) + + assert.deepEqual(result.addrs, ['127.0.0.1', '10.10.10.1']) + }) + + it('should parse [z-forwarded-host]', function () { + var req = request({'z-forwarded-host': 'mockbin.com'}) + var result = forwarded(req, options) + + assert.deepEqual(result.host, 'mockbin.com') + }) + + it('should parse [z-forwarded-port]', function () { + var req = request({'z-forwarded-port': '9000'}) + var result = forwarded(req, options) + + assert.deepEqual(result.port, '9000') + }) + + it('should parse [z-forwarded-proto]', function () { + var req = request({'z-forwarded-proto': 'https'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) + + it('should parse [z-forwarded-protocol]', function () { + var req = request({'z-forwarded-protocol': 'https'}) + var result = forwarded(req, options) + + assert.deepEqual(result.proto, 'https') + }) +}) diff --git a/test/server.js b/test/server.js new file mode 100644 index 0000000..1a4e011 --- /dev/null +++ b/test/server.js @@ -0,0 +1,35 @@ +/* globals describe, it */ + +'use strict' + +var assert = require('assert') +var request = require('./helpers').createRequestServer + +describe('live server', function () { + it('should get defaults', function (done) { + request(null, function (result, address) { + assert.ok(/127\.0\.0\.1:\d+/.test(result.host)) + assert.deepEqual(result.addrs, ['127.0.0.1']) + assert.equal(result.proto, 'http') + + done() + }) + }) + + it('should get defaults', function (done) { + var headers = { + 'host': 'mockbin.com', + 'forwarded': 'host=mockbin.com; for=0.0.0.1, for=0.0.0.2, for=private, for="1::8"; port=9000; proto=https; by=0.0.0.3' + } + + request(headers, function (result) { + assert.deepEqual(result.addrs, ['127.0.0.1', '1::8', 'private', '0.0.0.2', '0.0.0.1']) + assert.equal(result.by, '0.0.0.3') + assert.equal(result.host, 'mockbin.com') + assert.equal(result.proto, 'https') + + done() + }) + }) +}) + diff --git a/test/test.js b/test/test.js deleted file mode 100644 index f088791..0000000 --- a/test/test.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict' - -var assert = require('assert') -var forwarded = require('..') - -describe('forwarded(req)', function () { - it('should require req', function () { - assert.throws(forwarded.bind(null), /argument req.*required/) - }) - - it('should work with X-Forwarded-For header', function () { - var req = createReq('127.0.0.1') - assert.deepEqual(forwarded(req), ['127.0.0.1']) - }) - - it('should include entries from X-Forwarded-For', function () { - var req = createReq('127.0.0.1', { - 'x-forwarded-for': '10.0.0.2, 10.0.0.1' - }) - assert.deepEqual(forwarded(req), ['127.0.0.1', '10.0.0.1', '10.0.0.2']) - }) - - it('should skip blank entries', function () { - var req = createReq('127.0.0.1', { - 'x-forwarded-for': '10.0.0.2,, 10.0.0.1' - }) - assert.deepEqual(forwarded(req), ['127.0.0.1', '10.0.0.1', '10.0.0.2']) - }) -}) - -function createReq(socketAddr, headers) { - return { - connection: { - remoteAddress: socketAddr - }, - headers: headers || {} - }; -}