<|start_filename|>lib/index.js<|end_filename|>
'use strict';

const Hoek = require('hoek');
const Joi = require('joi');


const internals = {
    defaults: {
        redirectHttpToHttps: true,
        remoteAddressToClientIp: true,
        remotePortToClientPort: true,
        mapRequestId: true
    },
    options: Joi.object ({
        redirectHttpToHttps: Joi.boolean(),
        remoteAddressToClientIp: Joi.boolean(),
        remotePortToClientPort: Joi.boolean(),
        mapRequestId: Joi.boolean()
    })
};


exports.register = function (server, options, next) {

    const validateOptions = internals.options.validate(options);
    if(validateOptions.error) {

        return next(validateOptions.error);
    }

    const settings = Hoek.clone(internals.defaults);
    Hoek.merge(settings, options);

    if(!settings.remoteAddressToClientIp
        && !settings.remotePortToClientPort
        && !settings.redirectHttpToHttps) {

        // Plugin has nothing to do so immediately
        // continue.

        return next('Plugin has nothing to do. Please unregister it.');
    }

    server.ext('onRequest', function (request, reply) {

        // The heroku platform automatically routes requests sent to your application
        // to your dynos. Inbound requests are received by a load balancer and then routed
        // to your web dynos. The web dyno receives the request from the router so the
        // originating client ip/port are passed to your web dyno as the
        // 'x-forwarded-for' and 'x-forwarded-port '
        // See: https://devcenter.heroku.com/articles/http-routing#heroku-headers

        if(settings.remoteAddressToClientIp) {

            request.info.remoteAddress = request.headers['x-forwarded-for'] || request.info.remoteAddress;
        }

        if(settings.remotePortToClientPort) {

            request.info.remotePort = request.headers['x-forwarded-port'] || request.info.remotePort;
        }

        if(settings.mapRequestId) {
            request.id = request.headers['x-request-id'] || request.id;
        }

        // Heroku terminates ssl at the edge of the network so your hapi site will
        // always be running with a http binding in it's dyno. If you wanted the external
        // facing site to be https then it's necessary to redirect to https. The
        // x-forwarded-proto header is set by heroku to specify the originating protocol
        // of the http request
        // See: https://devcenter.heroku.com/articles/http-routing#heroku-headers

        if(settings.redirectHttpToHttps
            && request.method === 'get'
            && request.headers['x-forwarded-proto'] === 'http') {

            return reply()
                .redirect('https://' + request.headers.host + request.url.path)
                .permanent();
        }

        return reply.continue();
    });

    return next();
};

exports.register.attributes = {
    pkg: require('../package.json')
};


<|start_filename|>test/index.js<|end_filename|>
'use strict';

const Lab = require('lab');
const Hapi = require('hapi');
const Code = require('code');

const lab = exports.lab = Lab.script();
const expect = Code.expect;
const describe = lab.describe;
const it = lab.it;
const before = lab.before;


describe('Plugin registration', function () {

    it('successfully registers without requiring options', function (done) {

        const server = new Hapi.Server();
        server.connection();

        server.register(require('..'), function (err) {

            expect(err).to.not.exist();

            expect(server.registrations['hapi-heroku-helpers']).to.exist();
            expect(server.registrations['hapi-heroku-helpers'].version).to.equal(require('../package.json').version);

            done();
        });
    });

    it('asserts if there are invalid options', function (done) {

        const server = new Hapi.Server();
        server.connection();

        const plugin = {
            register: require('..'),
            options: 'weird options'
        };

        server.register(plugin, function (err) {

            expect(err).to.exist();

            done();
        });
    });

    it('asserts if there is nothing for the plugin to do', function (done) {

        const server = new Hapi.Server();
        server.connection();

        const plugin = {
            register: require('..'),
            options: {
                redirectHttpToHttps: false,
                remoteAddressToClientIp: false,
                remotePortToClientPort: false
            }
        };

        server.register(plugin, function (err) {

            expect(err).to.equal('Plugin has nothing to do. Please unregister it.');

            done();
        });
    });
});

describe('redirectHttpToHttps', function () {

    describe('enabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: ['GET', 'POST'],
                path: '/',
                handler: function (request, reply) {

                    return reply(true);
                }
            });

            server.route({
                method: ['GET', 'POST'],
                path: '/somepath',
                handler: function (request, reply) {

                    return reply(true);
                }
            });

            server.register(require('..'), done);
        });

        it('redirects to https when x-forwarded-proto is http', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-proto': 'http'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(301);
                expect(res.headers.location).to.startWith('https://');

                done();
            });
        });

        it('redirects contain path and query string', function (done) {

            const url = '/human?spud=ada';
            const requestOptions = {
                method: 'GET',
                url,
                headers: {
                    'x-forwarded-proto': 'http'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(301);
                expect(res.headers.location).to.startWith('https://');
                expect(res.headers.location).to.endWith(url);

                done();
            });
        });

        it('does not redirect POST requests even then when x-forwarded-proto is http', function (done) {

            const requestOptions = {
                method: 'POST',
                url: '/',
                headers: {
                    'x-forwarded-proto': 'http'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);

                done();
            });
        });

        it('does not redirect to https when x-forwarded-proto is not http', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-proto': 'random'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);

                done();
            });
        });

        it('does not redirect to https when x-forwarded-proto is not set', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/'
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);

                done();
            });
        });
    });

    describe('disabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(true);
                }
            });

            const plugin = {
                register: require('..'),
                options: {
                    redirectHttpToHttps: false
                }
            };

            server.register(plugin, done);
        });

        it('does not redirect to https even when x-forwarded-proto is http', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-proto': 'http'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);

                done();
            });
        });
    });
});


describe('remoteAddressToClientIp', function () {

    describe('enabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(request.info.remoteAddress);
                }
            });

            server.register(require('..'), done);
        });

        it('sets request.info.remoteAddress to the value of x-forwarded-for', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-for': '192.168.127.12'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal('192.168.127.12');

                done();
            });
        });

        it('does not change request.info.remoteAddress if there is no x-forwarded-for', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/'
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal('127.0.0.1');

                done();
            });
        });
    });

    describe('disabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(request.info.remoteAddress);
                }
            });

            const plugin = {
                register: require('..'),
                options: {
                    remoteAddressToClientIp: false
                }
            };

            server.register(plugin, done);
        });

        it('does not change request.info.remoteAddress even when x-forwarded-for is set', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-for': '192.168.127.12'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal('127.0.0.1');

                done();
            });
        });

    });
});


describe('remotePortToClientPort', function () {

    describe('enabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(request.info.remotePort);
                }
            });

            server.register(require('..'), done);
        });

        it('sets request.info.remotePort to the value of x-forwarded-for', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-port': '3781'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal('3781');

                done();
            });
        });

        it('does not change request.info.remotePort if there is no x-forwarded-port', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/'
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.be.null();

                done();
            });
        });
    });

    describe('disabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(request.info.remotePort);
                }
            });

            const plugin = {
                register: require('..'),
                options: {
                    remotePortToClientPort: false
                }
            };

            server.register(plugin, done);
        });

        it('does not change request.info.remotePort even when x-forwarded-port is set', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-forwarded-port': '3781'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal(null);

                done();
            });
        });
    });
});

describe('mapRequestId', function () {

    describe('enabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(request.id);
                }
            });

            server.register(require('..'), done);
        });

        it('sets request.id to the value of x-request-id', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-request-id': '1234'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.request.id).to.equal('1234');
                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal('1234');

                done();
            });
        });

        it('does not change request.id if there is no x-request-id', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/'
            };

            server.inject(requestOptions, function (res) {

                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal(res.request.id);

                done();
            });
        });
    });

    describe('disabled', function () {

        let server;

        before(function (done) {

            server = new Hapi.Server();
            server.connection();

            server.route({
                method: 'GET',
                path: '/',
                handler: function (request, reply) {

                    return reply(request.id);
                }
            });

            const plugin = {
                register: require('..'),
                options: {
                    mapRequestId: false
                }
            };

            server.register(plugin, done);
        });

        it('does not change request.id even when x-request-id is set', function (done) {

            const requestOptions = {
                method: 'GET',
                url: '/',
                headers: {
                    'x-request-id': '1234'
                }
            };

            server.inject(requestOptions, function (res) {

                expect(res.request.id).to.not.equal('1234');
                expect(res.statusCode).to.equal(200);
                expect(res.result).to.equal(res.request.id);

                done();
            });
        });
    });
});


<|start_filename|>package.json<|end_filename|>
{
  "name": "hapi-heroku-helpers",
  "version": "3.0.0",
  "repository": "git://github.com/briandela/hapi-heroku-helpers",
  "description": "hapi.js plugin which provides some basic functionality which can be useful when running a hapi.js site on Heroku.",
  "main": "lib/index.js",
  "author": "<NAME> <<EMAIL>>",
  "scripts": {
    "test": "lab -r lcov | node node_modules/coveralls/bin/coveralls.js; lab -a code -v -t 100 -L"
  },
  "engines": {
    "node": ">= 4.x.x"
  },
  "keywords": [
    "hapi",
    "plugin",
    "hapi-plugin",
    "hapi.js",
    "heroku",
    "https"
  ],
  "license": "MIT",
  "dependencies": {
    "hoek": "^4.1.0",
    "joi": "^10.0.6"
  },
  "devDependencies": {
    "code": "^4.x.x",
    "coveralls": "^2.x.x",
    "hapi": ">=16.x.x",
    "lab": "^11.x.x"
  }
}
