Skip to content

Commit

Permalink
Add initial module logic (#1)
Browse files Browse the repository at this point in the history
* Add initial module logic

* Convert to correct hapi plugin

* Add grunt tasks, lint config, update readme

* Fix lint

* PR Fixes
  • Loading branch information
vbudhram authored Oct 11, 2016
1 parent 628da10 commit 8e10f1b
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extends: fxa/server
19 changes: 19 additions & 0 deletions .jscsrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"disallowKeywords": ["with", "eval"],
"disallowKeywordsOnNewLine": ["else"],
"disallowMultipleLineStrings": true,
"disallowSpaceAfterObjectKeys": true,
"disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"],
"disallowSpaceBeforePostfixUnaryOperators": ["++", "--"],
"maximumLineLength": 160,
"requireCapitalizedConstructors": true,
"requireCurlyBraces": ["for", "while", "do"],
"requireLineFeedAtFileEnd": true,
"requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return"],
"requireSpaceAfterBinaryOperators": ["=", ",", "+", "-", "/", "*", "==", "===", "!=", "!=="],
"requireSpaceAfterPrefixUnaryOperators": ["~"],
"requireSpacesInConditionalExpression": true,
"requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
"validateIndentation": 2,
"validateQuoteMarks": "'"
}
4 changes: 4 additions & 0 deletions .nsprc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"exceptions": [
]
}
13 changes: 13 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = function (grunt) {
require('load-grunt-tasks')(grunt)

grunt.initConfig({
pkg: grunt.file.readJSON('./package.json'),
// .js files for ESLint, JSHint, JSCS, etc.
mainJsFiles: '{,grunttasks/,lib/**/,test/**/}*.js'
})

grunt.loadTasks('grunttasks')

grunt.registerTask('default', ['lint', 'nsp'])
}
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,29 @@
# hapi-hpkp
Hapi module to add HPKP headers to request
Inspired by [Helmetjs](https://github.com/helmetjs/hpkp), this is a Hapi module to add HPKP headers to all requests.

## Example

```javascript
var hpkp = require('./index')
var Hapi = require('hapi')

var hpkpOptions = {
maxAge: 1, // In seconds
sha256s: ["orlando=", "magic="], // Array of sha256
includeSubdomains: true, // optional
reportUri: 'http://test.site', // optional
reportOnly: false // optional
}

var server = new Hapi.Server()

// Register HPKP plugin
server.register({
register: hpkp,
options: hpkpOptions
}, function (err) {
if (err) {
console.error('Failed to load plugin:', err)
}
})
```
13 changes: 13 additions & 0 deletions grunttasks/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = function (grunt) {
'use strict'

grunt.config('eslint', {
options: {
eslintrc: '.eslintrc'
},
files: [
'{,grunttasks/,lib/**/,test/**/}*.js'
]
})
grunt.registerTask('quicklint', 'lint the modified files', 'newer:eslint')
}
12 changes: 12 additions & 0 deletions grunttasks/jscs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = function (grunt) {
'use strict'

grunt.config('jscs', {
app: [
'<%= mainJsFiles %>'
],
options: {
config: '.jscsrc'
}
})
}
10 changes: 10 additions & 0 deletions grunttasks/lint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// meta grunt task to run other linters.

module.exports = function (grunt) {
'use strict'

grunt.registerTask('lint', [
'eslint',
'jscs'
])
}
8 changes: 8 additions & 0 deletions grunttasks/nsp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = function (grunt) {
'use strict'

grunt.config('nsp', {
output: 'summary',
package: grunt.file.readJSON('package.json')
})
}
17 changes: 17 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Hapi middleware to append HPKP headers to all responses.
*
*/
'use strict'

var hpkp = require('./lib/hpkp')

exports.register = function (server, options, next) {
server.ext('onPreResponse', hpkp(options))

next()
}

exports.register.attributes = {
pkg: require('./package.json')
}
60 changes: 60 additions & 0 deletions lib/hpkp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Hapi middleware to append HPKP headers to all responses.
*
*/
var Joi = require('joi')

module.exports = function (options) {

var optionsSchema = Joi.object().keys({
maxAge: Joi.number().min(0).required(),
sha256s: Joi.array().min(1).items(Joi.string()).required(),
reportUri: Joi.string().uri().optional(),
reportOnly: Joi.boolean().optional(),
includeSubdomains: Joi.boolean().optional()
})

var error = optionsSchema.validate(options).error
if (error) {
throw new Error(error)
}

var sha256s = options.sha256s
var maxAge = options.maxAge
var includeSubdomains = options.includeSubdomains
var reportOnly = options.reportOnly
var reportUri = options.reportUri

var hpkpParts = []

sha256s.forEach(function (shaPin) {
hpkpParts.push('pin-sha256="' + shaPin + '"')
})

hpkpParts.push('max-age=' + maxAge)

if (includeSubdomains) {
hpkpParts.push('includeSubdomains')
}

if (reportUri) {
hpkpParts.push('report-uri="' + reportUri + '"')
}

var hpkpHeaderKey = 'Public-Key-Pins'
if (reportOnly) {
hpkpHeaderKey = 'Public-Key-Pins-Report-Only'
}

var hpkpHeader = hpkpParts.join('; ')

return function (request, reply) {
var response = request.response

if (response.header) {
response.header(hpkpHeaderKey, hpkpHeader)
}

return reply.continue()
}
}
38 changes: 38 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "hapi-hpkp",
"version": "1.0.0",
"description": "Hapi module to add HPKP headers",
"main": "index.js",
"scripts": {
"test": "node ./node_modules/.bin/mocha tests/index.js",
"lint": "grunt lint"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vbudhram/hapi-hpkp.git"
},
"keywords": [
"hapi",
"hpkp"
],
"author": "Vijay Budhram",
"license": "MIT",
"bugs": {
"url": "https://github.com/vbudhram/hapi-hpkp/issues"
},
"homepage": "https://github.com/vbudhram/hapi-hpkp#readme",
"dependencies": {
"joi": "9.0.4"
},
"devDependencies": {
"chai": "3.5.0",
"eslint-config-fxa": "2.1.0",
"hapi": "15.0.2",
"grunt": "1.0.1",
"grunt-eslint": "19.0.0",
"grunt-jscs": "3.0.1",
"grunt-nsp": "2.3.1",
"load-grunt-tasks": "3.5.2",
"mocha": "3.0.2"
}
}
136 changes: 136 additions & 0 deletions tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
var assert = require('chai').assert
var hpkp = require('../lib/hpkp')
var Hapi = require('hapi')
var server

function createServer(port, hpkpOptions) {
server = new Hapi.Server()
server.connection({
port: port
})

server.register({
register: require('../index.js'),
options: hpkpOptions
}, function (err) {
if (err) {
console.error('Failed to load plugin:', err)
}
});

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

server.start()

return server
}

var sha256s = [
'orlando=',
'magic='
]

var passingTestCases = [
{
name: 'should process minimum HPKP header',
options: {
maxAge: 1,
sha256s: sha256s
},
expectedKey: 'public-key-pins',
expectedHeader: 'pin-sha256="orlando="; pin-sha256="magic="; max-age=1'
},
{
name: 'should process with includeSubdomains',
options: {
maxAge: 1,
sha256s: sha256s,
includeSubdomains: true
},
expectedKey: 'public-key-pins',
expectedHeader: 'pin-sha256="orlando="; pin-sha256="magic="; max-age=1; includeSubdomains'
},
{
name: 'should process with reportOnly',
options: {
maxAge: 1,
sha256s: sha256s,
reportOnly: true
},
expectedKey: 'public-key-pins-report-only',
expectedHeader: 'pin-sha256="orlando="; pin-sha256="magic="; max-age=1'
},
{
name: 'should process with report-uri',
options: {
maxAge: 1,
sha256s: sha256s,
reportOnly: true,
reportUri: 'http://test.site'
},
expectedKey: 'public-key-pins-report-only',
expectedHeader: 'pin-sha256="orlando="; pin-sha256="magic="; max-age=1; report-uri="http://test.site"'
}
]

describe('HPKP Headers', function () {
passingTestCases.forEach(function (testCase) {
var server
var requestOptions = {
method: "GET",
url: "/"
}
before(function () {
server = createServer(3000, testCase.options)
})

after(function () {
return server.stop()
})
it(testCase.name, function (done) {
server.inject(requestOptions, function (response) {
assert.equal(response.headers[testCase.expectedKey], testCase.expectedHeader)
done()
})
})
})
})

var failingTestCases = [
{
name: 'should throw without any options',
options: {},
message: 'ValidationError: child "maxAge" fails because ["maxAge" is required]'
},
{
name: 'should throw without sha256s',
options: {
maxAge: 1
},
message: 'ValidationError: child "sha256s" fails because ["sha256s" is required]'
},
{
name: 'should throw with empty sha256s',
options: {
maxAge: 1,
sha256s: []
},
message: 'ValidationError: child "sha256s" fails because ["sha256s" must contain at least 1 items]'
}
]

describe('HPKP Config', function () {
failingTestCases.forEach(function (testCase) {
it(testCase.name, function () {
assert.throws(function () {
hpkp(testCase.options)
}, testCase.message, 'threw error')
})
})
})

0 comments on commit 8e10f1b

Please sign in to comment.