Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add request.reply.proxy() #1147

Merged
merged 2 commits into from
Nov 15, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
- [`request.reply.redirect(uri)`](#requestreplyredirecturi)
- [`request.reply.view(template, [context, [options]])`](#requestreplyviewtemplate-context-options)
- [`request.reply.close()`](#requestreplyclose)
- [`request.reply.proxy(options)`](#requestreplyproxyoptions)
- [`request.generateView(template, context, [options])`](#requestgenerateviewtemplate-context-options)
- [`request.response()`](#requestresponse)
- [`Hapi.response`](#hapiresponse)
Expand Down Expand Up @@ -399,12 +400,13 @@ The following options are available when adding a route:
- `lookupCompressed` - optional boolean, instructs the file processor to look for the same filename with the '.gz' suffix for a precompressed
version of the file to serve if the request supports content encoding. Defaults to `false`.

- `proxy` - generates a reverse proxy handler with the following options:
- <a name="route.config.proxy"></a>`proxy` - generates a reverse proxy handler with the following options:
- `host` - the upstream service host to proxy requests to. The same path on the client request will be used as the path on the host.
- `port` - the upstream service port.
- `protocol` - The protocol to use when making a request to the proxied host:
- `'http'`
- `'https'`
- `uri` - an absolute URI used instead of the incoming host, port, protocol, path, and query. Cannot be used with `host`, `port`, `protocol`, or `mapUri`.
- `passThrough` - if `true`, forwards the headers sent from the client to the upstream service being proxied to. Defaults to `false`.
- `rejectUnauthorized` - sets the `rejectUnauthorized` property on the https [agent](http://nodejs.org/api/https.html#https_https_request_options_callback)
making the request. This value is only used when the proxied server uses TLS/SSL. When set it will override the node.js `rejectUnauthorized` property.
Expand All @@ -417,7 +419,7 @@ The following options are available when adding a route:
no redirections (301, 302, 307, 308) will be passed along to the client, and reaching the maximum allowed redirections will return an
error response. Defaults to `false`.
- `timeout` - number of milliseconds before aborting the upstream request. Defaults to `180000` (3 minutes).
- `mapUri` - a function used to map the request URI to the proxied URI. Cannot be used together with `host`, `port`, or `protocol`.
- `mapUri` - a function used to map the request URI to the proxied URI. Cannot be used together with `host`, `port`, `protocol`, or `uri`.
The function signature is `function(request, callback)` where:
- `request` - is the incoming `request` object
- `callback` - is `function(err, uri, headers)` where:
Expand Down Expand Up @@ -1708,8 +1710,8 @@ request.clearState('preferences');

#### `request.reply([result])`

_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`, or
`request.reply.close()` is called._
_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`,
`request.reply.close()`, or `request.reply.proxy()` is called._

Concludes the handler activity by returning control over to the router where:

Expand Down Expand Up @@ -1747,8 +1749,8 @@ The [response flow control rules](#flow-control) apply.

##### `request.reply.redirect(uri)`

_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`, or
`request.reply.close()` is called._
_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`,
`request.reply.close()`, or `request.reply.proxy()` is called._

Concludes the handler activity by returning control over to the router with a redirection response where:

Expand All @@ -1770,8 +1772,8 @@ The [response flow control rules](#flow-control) apply.

##### `request.reply.view(template, [context, [options]])`

_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`, or
`request.reply.close()` is called._
_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`,
`request.reply.close()`, or `request.reply.proxy()` is called._

Concludes the handler activity by returning control over to the router with a templatized view response where:

Expand Down Expand Up @@ -1823,8 +1825,8 @@ The [response flow control rules](#flow-control) apply.

##### `request.reply.close()`

_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`, or
`request.reply.close()` is called._
_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`,
`request.reply.close()`, or `request.reply.proxy()` is called._

Concludes the handler activity by returning control over to the router and informing the router that a response has already been sent back
directly via `request.raw.res` and that no further response action is needed (the router will ensure the `request.raw.res` was ended).
Expand All @@ -1833,6 +1835,25 @@ No return value.

The [response flow control rules](#flow-control) **do not** apply.

##### `request.reply.proxy(options)`

_Available only within the handler method and only before one of `request.reply()`, `request.reply.redirection()`, `request.reply.view()`,
`request.reply.close()`, or `request.reply.proxy()` is called._

Proxies the request to an upstream endpoint where:
- `options` - an object including the same keys and restrictions defined by the [route `proxy` handler options](#route.config.proxy).

No return value.

The [response flow control rules](#flow-control) **do not** apply.

```javascript
var handler = function () {

this.reply.proxy({ host: 'example.com', port: 80, protocol: 'http' });
};
```

#### `request.generateView(template, context, [options])`

_Always available._
Expand Down
57 changes: 32 additions & 25 deletions lib/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,22 @@ var Utils = require('./utils');
var internals = {};


// Create and configure server instance

exports = module.exports = internals.Proxy = function (options, route) {
exports.handler = function (options, route) {

Utils.assert(!options.passThrough || !route.settings.cache.mode.server, 'Cannot use pass-through proxy mode with caching');

this.settings = Utils.applyToDefaults(Defaults.proxy, options);
this.settings.mapUri = options.mapUri || internals.mapUri(options.protocol, options.host, options.port);
this.settings.isCustomPostResponse = !!options.postResponse;
this.settings.postResponse = options.postResponse || internals.postResponse; // function (request, settings, response, payload)
var settings = Utils.applyToDefaults(Defaults.proxy, options);
settings.mapUri = options.mapUri || internals.mapUri(options.protocol, options.host, options.port, options.uri);
settings.isCustomPostResponse = !!options.postResponse;
settings.postResponse = options.postResponse || internals.postResponse; // function (request, settings, response, payload)

if (options.rejectUnauthorized !== undefined) {
this.settings.rejectUnauthorized = options.rejectUnauthorized;
settings.rejectUnauthorized = options.rejectUnauthorized;
}
};


internals.Proxy.prototype.handler = function () {

var self = this;

return function (request) {

self.settings.mapUri(request, function (err, uri, headers) {
settings.mapUri(request, function (err, uri, headers) {

if (err) {
return request.reply(err);
Expand All @@ -45,13 +37,13 @@ internals.Proxy.prototype.handler = function () {
var options = {
headers: {},
payload: null,
redirects: self.settings.redirects,
timeout: self.settings.timeout,
rejectUnauthorized: self.settings.rejectUnauthorized,
redirects: settings.redirects,
timeout: settings.timeout,
rejectUnauthorized: settings.rejectUnauthorized,
downstreamRes: request.raw.res
};

if (self.settings.passThrough) { // Never set with cache
if (settings.passThrough) { // Never set with cache
options.headers = Utils.clone(req.headers);
delete options.headers.host;
}
Expand All @@ -60,13 +52,13 @@ internals.Proxy.prototype.handler = function () {
Utils.merge(options.headers, headers);
}

if (self.settings.xforward) {
if (settings.xforward) {
options.headers['x-forwarded-for'] = (options.headers['x-forwarded-for'] ? options.headers['x-forwarded-for'] + ',' : '') + request.info.remoteAddress;
options.headers['x-forwarded-port'] = (options.headers['x-forwarded-port'] ? options.headers['x-forwarded-port'] + ',' : '') + request.info.remotePort;
options.headers['x-forwarded-proto'] = (options.headers['x-forwarded-proto'] ? options.headers['x-forwarded-proto'] + ',' : '') + self.settings.protocol;
options.headers['x-forwarded-proto'] = (options.headers['x-forwarded-proto'] ? options.headers['x-forwarded-proto'] + ',' : '') + settings.protocol;
}

var isParsed = (self.settings.isCustomPostResponse || request.route.cache.mode.server);
var isParsed = (settings.isCustomPostResponse || request.route.cache.mode.server);
if (isParsed) {
delete options.headers['accept-encoding'];
}
Expand Down Expand Up @@ -100,15 +92,31 @@ internals.Proxy.prototype.handler = function () {
return request.reply(err);
}

return self.settings.postResponse(request, self.settings, res, buffer.toString());
return settings.postResponse(request, settings, res, buffer.toString());
});
});
});
};
};


internals.mapUri = function (protocol, host, port) {
internals.mapUri = function (protocol, host, port, uri) {

if (uri) {
return function (request, next) {

if (uri.indexOf('{') === -1) {
return next(null, uri);
}

var address = uri.replace(/{protocol}/g, request.server.info.protocol)
.replace(/{host}/g, request.server.info.host)
.replace(/{port}/g, request.server.info.port)
.replace(/{path}/g, request.url.path);

return next(null, address);
};
}

protocol = protocol || 'http';
port = port || (protocol === 'http' ? 80 : 443);
Expand All @@ -134,4 +142,3 @@ internals.postResponse = function (request, settings, res, payload) {
response.type(contentType);
}
};

7 changes: 7 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var Response = require('./response');
var Cached = require('./response/cached');
var Closed = require('./response/closed');
var Ext = require('./ext');
var Proxy = require('./proxy');


// Declare internals
Expand Down Expand Up @@ -631,6 +632,12 @@ internals.Request.prototype._decorateReply = function (finalize) {
return Response._generate(new Response.View(viewsManager, template, context, options), self, finalize);
};
}

this.reply.proxy = function (options) {

var handler = Proxy.handler(options, self._route);
handler.call(self, self, self.reply);
};
};


Expand Down
2 changes: 1 addition & 1 deletion lib/response/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ internals.Stream.prototype._prepare = function (request, callback) {
// Apply passthrough headers

if (self._passThrough.headers &&
(!request._route.proxy || request._route.proxy.settings.passThrough)) {
(!request._route.proxy || request._route.proxy.passThrough)) {

var localCookies = Utils.clone(self._headers['set-cookie']);
var localHeaders = self._headers;
Expand Down
4 changes: 2 additions & 2 deletions lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ exports = module.exports = internals.Route = function (options, server, env) {

if (typeof this.settings.handler === 'object') {
if (this.settings.handler.proxy) {
this.proxy = new Proxy(this.settings.handler.proxy, this);
this.settings.handler = this.proxy.handler();
this.proxy = this.settings.handler.proxy;
this.settings.handler = Proxy.handler(this.settings.handler.proxy, this);
}
else if (this.settings.handler.file) {
this.settings.handler = Files.fileHandler(this, this.settings.handler.file);
Expand Down
9 changes: 5 additions & 4 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,16 @@ internals.routeConfigSchema = {
}),
Joi.object({
proxy: Joi.object({
host: Joi.string().xor('mapUri'),
port: Joi.number().integer().without('mapUri'),
protocol: Joi.string().valid('http', 'https').without('mapUri'),
host: Joi.string().xor('mapUri', 'uri'),
port: Joi.number().integer().without('mapUri', 'uri'),
protocol: Joi.string().valid('http', 'https').without('mapUri', 'uri'),
uri: Joi.string().without('host', 'port', 'protocol', 'mapUri'),
passThrough: Joi.boolean(),
rejectUnauthorized: Joi.boolean(),
xforward: Joi.boolean(),
redirects: Joi.number().min(0).integer().allow(false),
timeout: Joi.number().integer(),
mapUri: Joi.func().without('host', 'port', 'protocol'),
mapUri: Joi.func().without('host', 'port', 'protocol', 'uri'),
postResponse: Joi.func()
}).required()
}),
Expand Down
41 changes: 39 additions & 2 deletions test/integration/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ describe('Proxy', function () {
{ method: 'POST', path: '/post2', handler: function () { this.reply(this.payload); } },
{ method: 'GET', path: '/cached', handler: profile },
{ method: 'GET', path: '/timeout1', handler: timeoutHandler },
{ method: 'GET', path: '/timeout2', handler: timeoutHandler }
{ method: 'GET', path: '/timeout2', handler: timeoutHandler },
{ method: 'GET', path: '/handlerOldSchool', handler: activeItem }
]);

var upstreamSsl = new Hapi.Server(0, { tls: tlsOptions });
Expand Down Expand Up @@ -225,7 +226,10 @@ describe('Proxy', function () {
{ method: 'GET', path: '/cached', handler: { proxy: { host: 'localhost', port: backendPort } }, config: { cache: routeCache } },
{ method: 'GET', path: '/timeout1', handler: { proxy: { host: 'localhost', port: backendPort, timeout: 5 } } },
{ method: 'GET', path: '/timeout2', handler: { proxy: { host: 'localhost', port: backendPort } } },
{ method: 'GET', path: '/single', handler: { proxy: { mapUri: mapSingleUri } } }
{ method: 'GET', path: '/single', handler: { proxy: { mapUri: mapSingleUri } } },
{ method: 'GET', path: '/handler', handler: function () { this.reply.proxy({ uri: 'http://localhost:' + backendPort + '/item' }); } },
{ method: 'GET', path: '/handlerTemplate', handler: function () { this.reply.proxy({ uri: '{protocol}://localhost:' + backendPort + '/item' }); } },
{ method: 'GET', path: '/handlerOldSchool', handler: function () { this.reply.proxy({ host: 'localhost', port: backendPort }); } }
]);

sslServer = new Hapi.Server(0);
Expand Down Expand Up @@ -640,5 +644,38 @@ describe('Proxy', function () {
done();
});
});

it('proxies via request.reply.proxy()', function (done) {

server.inject('/handler', function (res) {

expect(res.statusCode).to.equal(200);
expect(res.payload).to.contain('Active Item');
var counter = res.result.count;
done();
});
});

it('proxies via request.reply.proxy() with uri tempalte', function (done) {

server.inject('/handlerTemplate', function (res) {

expect(res.statusCode).to.equal(200);
expect(res.payload).to.contain('Active Item');
var counter = res.result.count;
done();
});
});

it('proxies via request.reply.proxy() with individual options', function (done) {

server.inject('/handlerOldSchool', function (res) {

expect(res.statusCode).to.equal(200);
expect(res.payload).to.contain('Active Item');
var counter = res.result.count;
done();
});
});
});