├── .jshintrc ├── README.md ├── circle.yml ├── index.js ├── package.json └── test ├── middleware-test.js └── test-helper.js /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "-W124": true, 3 | "esnext": true, 4 | "node": true, 5 | "unused": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # koa-ssl 2 | 3 | [![Circle CI](https://circleci.com/gh/jclem/koa-ssl.svg?style=svg)](https://circleci.com/gh/jclem/koa-ssl) 4 | 5 | koa-ssl enforces SSL for [Koa][koa] apps. 6 | 7 | ## Use 8 | 9 | Simply require and use the function exported by this module: 10 | 11 | ```javascript 12 | var ssl = require('koa-ssl'); 13 | var app = require('koa')(); 14 | app.use(ssl()); 15 | ``` 16 | 17 | The function takes an optional object of options: 18 | 19 | - `disabled`: (default `false`) If `true`, this middleware will allow all 20 | requests through. 21 | - `trustProxy`: (default `false`) If `true`, trust the `x-forwarded-proto` 22 | header. If it is "https", requests are allowed through. 23 | - `disallow`: A non-Generator function called with the Koa context so that the 24 | user can handle rejecting non-SSL requests themselves. 25 | 26 | By default, this middleware will only run when `process.env.NODE_ENV` is set to 27 | "production". Unless a `disallow` function is supplied it will respond with the 28 | status code 403 and the body "Please use HTTPS when communicating with this 29 | server." 30 | 31 | ## Thanks, Heroku 32 | 33 | While I created and maintain this project, it was done while I was an employee 34 | of [Heroku][heroku] on the Human Interfaces Team, and they were kind enough to 35 | allow me to open source the work. Heroku is awesome. 36 | 37 | [heroku]: https://www.heroku.com/home 38 | [koa]: https://github.com/koajs/koa 39 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | pre: 3 | - curl https://raw.githubusercontent.com/creationix/nvm/v0.23.3/install.sh | bash 4 | node: 5 | version: iojs-1.2.0 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function ssl(options) { 4 | options = options || {}; 5 | 6 | let enabled = options.hasOwnProperty('disabled') ? 7 | !options.disabled : process.env.NODE_ENV === 'production'; 8 | 9 | return sslMiddleware; 10 | 11 | /* jshint validthis: true */ 12 | function* sslMiddleware(next) { 13 | if (!enabled) return yield next; 14 | 15 | let isSecure = this.secure; 16 | 17 | if (!isSecure && options.trustProxy) { 18 | isSecure = this.get('x-forwarded-proto') === 'https'; 19 | } 20 | 21 | if (isSecure) { 22 | yield next; 23 | } else if (typeof options.disallow === 'function') { 24 | options.disallow(this); 25 | } else { 26 | this.status = 403; 27 | this.type = 'text/plain'; 28 | this.body = 'Please use HTTPS when communicating with this server.'; 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-ssl", 3 | "description": "Enforce SSL for koa apps", 4 | "version": "2.0.1", 5 | "author": "Jonathan Clem", 6 | "bugs": { 7 | "url": "https://github.com/jclem/koa-ssl/issues" 8 | }, 9 | "devDependencies": { 10 | "co": "^3.0.5", 11 | "koa": "^0.5.5", 12 | "mocha": "^2.1.0", 13 | "request": "^2.34.0", 14 | "should": "^3.3.1", 15 | "thunkify": "^2.1.1" 16 | }, 17 | "engines": { 18 | "iojs": "1.2.0", 19 | "node": ">= 7.4.0" 20 | }, 21 | "homepage": "https://github.com/jclem/koa-ssl", 22 | "keywords": [ 23 | "ssl", 24 | "koa" 25 | ], 26 | "license": "MIT", 27 | "main": "index.js", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/jclem/koa-ssl" 31 | }, 32 | "scripts": { 33 | "test": "mocha" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/middleware-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint mocha: true */ 4 | 5 | let co = require('co'); 6 | let thunk = require('thunkify'); 7 | let app = require('./test-helper').app; 8 | let createServer = require('./test-helper').createServer; 9 | let ssl = require('../index'); 10 | 11 | require('should'); 12 | 13 | describe('SSL middleware', function() { 14 | let uri, server; 15 | 16 | afterEach(function() { 17 | process.env.NODE_ENV = 'test'; 18 | app.middleware = []; 19 | server.close(); 20 | }); 21 | 22 | function* setup(sslArgs) { 23 | app.use(ssl(sslArgs)); 24 | app.use(function*() { this.body = 'ok'; }); 25 | server = yield createServer(); 26 | uri = 'http://localhost:' + server.address().port; 27 | } 28 | 29 | function get(headers) { 30 | headers = headers || {}; 31 | 32 | return thunk(require('request').get)(uri, { 33 | headers: headers, 34 | followRedirect: false 35 | }); 36 | } 37 | 38 | describe('with no arguments', function() { 39 | describe('and NODE_ENV is not production', function() { 40 | it('does not redirect', co(function*() { 41 | yield setup(); 42 | let res = yield get(); 43 | res[0].statusCode.should.eql(200); 44 | })); 45 | }); 46 | 47 | describe('and NODE_ENV is production', function() { 48 | it('responds with a 403', co(function*() { 49 | process.env.NODE_ENV = 'production'; 50 | yield setup(); 51 | let res = yield get(); 52 | res[0].statusCode.should.eql(403); 53 | res[1].should.eql('Please use HTTPS when communicating with this server.'); 54 | })); 55 | }); 56 | }); 57 | 58 | describe('when trustProxy is true', function() { 59 | describe('and x-forwarded-proto is http', function() { 60 | describe('and NODE_ENV is not production', function() { 61 | it('does not redirect', co(function*() { 62 | yield setup({ trustProxy: true }); 63 | let res = yield get(); 64 | res[0].statusCode.should.eql(200); 65 | })); 66 | }); 67 | 68 | describe('and NODE_ENV is production', function() { 69 | it('responds with a 403', co(function*() { 70 | process.env.NODE_ENV = 'production'; 71 | yield setup({ trustProxy: true }); 72 | let res = yield get(); 73 | res[0].statusCode.should.eql(403); 74 | res[1].should.eql('Please use HTTPS when communicating with this server.'); 75 | })); 76 | }); 77 | }); 78 | 79 | describe('and x-forwarded-proto is https', function() { 80 | describe('and NODE_ENV is not production', function() { 81 | it('does not redirect', co(function*() { 82 | yield setup({ trustProxy: true }); 83 | let res = yield get({ 'x-forwarded-proto': 'https' }); 84 | res[0].statusCode.should.eql(200); 85 | })); 86 | }); 87 | 88 | describe('and NODE_ENV is production', function() { 89 | it('does not redirect', co(function*() { 90 | process.env.NODE_ENV = 'production'; 91 | yield setup({ trustProxy: true }); 92 | let res = yield get({ 'x-forwarded-proto': 'https' }); 93 | res[0].statusCode.should.eql(200); 94 | })); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('when disallow is set', function() { 100 | it('calls the disallow function', co(function*() { 101 | yield setup({ disallow: disallow, disabled: false }); 102 | let res = yield get(); 103 | res[0].statusCode.should.eql(403); 104 | res[1].should.eql('Disallowed.'); 105 | 106 | function disallow(ctx) { 107 | ctx.status = 403; 108 | ctx.body = 'Disallowed.'; 109 | } 110 | })); 111 | }); 112 | 113 | describe('when disabled is false', function() { 114 | it('responds with a 403', co(function*() { 115 | yield setup({ disabled: false }); 116 | let res = yield get(); 117 | res[0].statusCode.should.eql(403); 118 | res[1].should.eql('Please use HTTPS when communicating with this server.'); 119 | })); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/test-helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'test'; 4 | process.env.SILENCE_LOGS = 'true'; 5 | 6 | let http = require('http'); 7 | let koa = require('koa'); 8 | let app = koa(); 9 | 10 | exports.app = app; 11 | 12 | exports.createServer = function*() { 13 | let server = http.createServer(app.callback()); 14 | yield listen(); 15 | return server; 16 | 17 | function listen() { 18 | return function(cb) { 19 | server.listen(cb); 20 | }; 21 | } 22 | }; 23 | --------------------------------------------------------------------------------