Skip to content

Commit

Permalink
Merge 553fb6f into d125b19
Browse files Browse the repository at this point in the history
  • Loading branch information
Ahmad Nassri committed May 31, 2015
2 parents d125b19 + 553fb6f commit ed5fec4
Show file tree
Hide file tree
Showing 26 changed files with 882 additions and 67 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -20,23 +22,48 @@ $ 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 | `['xff', 'rfc7239']` |

Parse appropriate headers from the request matching the selected [schemas](#options).

### 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

```sh
$ npm test
```

## TODO
- [ ] process [`Via`](http://tools.ietf.org/html/rfc7230#section-5.7.1) header
- [ ] extract ports from [`Forwarded`](http://tools.ietf.org/html/rfc7239#section-5.2) header: `Forwarded: for=x.x.x.x:yyyy`

## License

[MIT](LICENSE)
Expand Down
80 changes: 66 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*!
* forwarded
* Copyright(c) 2014 Douglas Christopher Wilson
* Copyright(c) 2015 Ahmad Nassri
* MIT Licensed
*/
*/

'use strict'

Expand All @@ -11,29 +12,80 @@
* @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 common + standard
// array order matters here
schemas: options.schemas || [
'rfc7239'
]
}

// consistent case
opts.schemas = opts.schemas.map(Function.prototype.call, String.prototype.toLowerCase)

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) {
if (!schemas[name]) {
throw new Error('invalid schema')
}

return schemas[name]
})

// process schemas
.reduce(function (forwarded, schema) {
var result = new Processor(req, schema)

// ensure reverse order of addresses
if (result.addrs) {
result.addrs.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)
}
82 changes: 82 additions & 0 deletions lib/processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict'

var debug = require('debug')('forwarded')

function processor (req, schema) {
if (typeof schema === 'function') {
return schema(req)
}

this.req = req
this.schema = schema

return {
addrs: this.addrs(),
host: this.host(),
port: this.port(),
proto: this.protocol()
}
}

processor.prototype.host = function () {
if (this.schema.host && this.req.headers[this.schema.host]) {
var value = this.req.headers[this.schema.host]

debug('found header [%s = %s]', this.schema.host, value)

return this.req.headers[this.schema.host]
}
}

processor.prototype.port = function () {
if (this.schema.port && this.req.headers[this.schema.port]) {
var value = this.req.headers[this.schema.port]

debug('found header [%s = %s]', this.schema.port, value)

return value
}
}

processor.prototype.addrs = function () {
if (this.schema.addrs && this.req.headers[this.schema.addrs]) {
var value = this.req.headers[this.schema.addrs]

debug('found header [%s = %s]', this.schema.addrs, value)

return value
.split(/ *, */)
.filter(Boolean)
.reverse()
}
}

processor.prototype.protocol = function () {
// utility
function runner (obj) {
// multiple possible values
if (Array.isArray(obj)) {
// get the last succesful item
return obj.map(runner.bind(this)).reduce(function (prev, curr) {
return curr ? curr : prev
})
}

if (typeof obj === 'function') {
return obj.call(this, this.req)
}

if (this.req.headers[obj]) {
debug('found header [%s = %s]', obj, this.req.headers[obj])

return this.req.headers[obj]
}
}

// actually run
if (this.schema.proto) {
return runner.call(this, this.schema.proto)
}
}

module.exports = processor
19 changes: 19 additions & 0 deletions lib/schemas/cloudflare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict'

var debug = require('debug')('forwarded')

function isSecure (req) {
try {
var cf = JSON.parse(req.headers['cf-visitor'])
return !~[undefined, 'http'].indexOf(cf.scheme)
} catch (e) {
debug('could not parse "cf-visitor" header: %s', req.headers['cf-visitor'])
}

return false
}

module.exports = {
addrs: 'cf-connecting-ip',
proto: isSecure
}
11 changes: 11 additions & 0 deletions lib/schemas/fastly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict'

function isSecure (req) {
return req.headers['fastly-ssl'] !== undefined
}

module.exports = {
addrs: 'fastly-client-ip',
port: 'fastly-client-port',
proto: isSecure
}
12 changes: 12 additions & 0 deletions lib/schemas/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict'

module.exports = {
cloudflare: require('./cloudflare'),
fastly: require('./fastly'),
microsoft: require('./microsoft'),
nginx: require('./nginx'),
rackspace: require('./rackspace'),
rfc7239: require('./rfc7239'),
xff: require('./xff'),
zscaler: require('./zscaler')
}
7 changes: 7 additions & 0 deletions lib/schemas/microsoft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict'

module.exports = {
proto: function isSecure (req) {
return req.headers['front-end-https'] === 'on'
}
}
7 changes: 7 additions & 0 deletions lib/schemas/nginx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict'

module.exports = {
addrs: 'x-real-ip',
port: 'x-real-port',
proto: ['x-real-proto', 'x-url-scheme']
}
5 changes: 5 additions & 0 deletions lib/schemas/rackspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict'

module.exports = {
addrs: 'x-cluster-client-ip'
}
59 changes: 59 additions & 0 deletions lib/schemas/rfc7239.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict'

function splitMap (string, separator, cb) {
// split into elements
return string.split(separator)
.filter(Boolean)
.forEach(cb)
}

function parsePart (part) {
var pair = part.split(/ *= */)

var name = pair[0].toLowerCase()
var value = pair[1]

if (value) {
switch (typeof this[name]) {
case 'undefined':
this[name] = value
break

// convert to array
case 'string':
this[name] = [this[name], value]
break

// append to array
case 'object':
this[name].push(value)
break
}
}
}

var ELEMENT_SEPARATOR = / *; */
var PART_SEPARATOR = / *, */

module.exports = function (req) {
var forwarded = {}
var header = req.headers.forwarded

if (!header) {
return forwarded
}

splitMap(header, ELEMENT_SEPARATOR, function parseElement (el) {
return splitMap(el, PART_SEPARATOR, parsePart.bind(forwarded))
})

// ensure result is an array
if (forwarded.for && !Array.isArray(forwarded.for)) {
forwarded.for = [forwarded.for]
}

// create alias
forwarded.addrs = forwarded.for

return forwarded
}

0 comments on commit ed5fec4

Please sign in to comment.