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

Replace hawk dependency with a local implemenation #2943

Merged
merged 8 commits into from May 19, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 89 additions & 0 deletions lib/hawk.js
@@ -0,0 +1,89 @@
'use strict'

var crypto = require('crypto')

function randomString (size) {
var bits = (size + 1) * 6
var buffer = crypto.randomBytes(Math.ceil(bits / 8))
var string = buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
return string.slice(0, size)
}

function calculatePayloadHash (payload, algorithm, contentType) {
var hash = crypto.createHash(algorithm)
hash.update('hawk.1.payload\n')
hash.update((contentType ? contentType.split(';')[0].trim().toLowerCase() : '') + '\n')
hash.update(payload || '')
hash.update('\n')
return hash.digest('base64')
}

exports.calculateMac = function (credentials, opts) {
var normalized = 'hawk.1.header\n' +
opts.ts + '\n' +
opts.nonce + '\n' +
(opts.method || '').toUpperCase() + '\n' +
opts.resource + '\n' +
opts.host.toLowerCase() + '\n' +
opts.port + '\n' +
(opts.hash || '') + '\n'

if (opts.ext) {
normalized = normalized + opts.ext.replace('\\', '\\\\').replace('\n', '\\n')
}

normalized = normalized + '\n'

if (opts.app) {
normalized = normalized + opts.app + '\n' + (opts.dlg || '') + '\n'
}

var hmac = crypto.createHmac(credentials.algorithm, credentials.key).update(normalized)
var digest = hmac.digest('base64')
return digest
}

exports.header = function (uri, method, opts) {
var timestamp = opts.timestamp || Math.floor((Date.now() + (opts.localtimeOffsetMsec || 0)) / 1000)
var credentials = opts.credentials
if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) {
return ''
}

if (['sha1', 'sha256'].indexOf(credentials.algorithm) === -1) {
return ''
}

var artifacts = {
ts: timestamp,
nonce: opts.nonce || randomString(6),
method: method,
resource: uri.pathname + (uri.search || ''),
host: uri.hostname,
port: uri.port || (uri.protocol === 'http:' ? 80 : 443),
hash: opts.hash,
ext: opts.ext,
app: opts.app,
dlg: opts.dlg
}

if (!artifacts.hash && (opts.payload || opts.payload === '')) {
artifacts.hash = calculatePayloadHash(opts.payload, credentials.algorithm, opts.contentType)
}

var mac = exports.calculateMac(credentials, artifacts)

var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''
var header = 'Hawk id="' + credentials.id +
'", ts="' + artifacts.ts +
'", nonce="' + artifacts.nonce +
(artifacts.hash ? '", hash="' + artifacts.hash : '') +
(hasExt ? '", ext="' + artifacts.ext.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : '') +
'", mac="' + mac + '"'

if (artifacts.app) {
header = header + ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'
}

return header
}
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -35,7 +35,6 @@
"forever-agent": "~0.6.1",
"form-data": "~2.3.1",
"har-validator": "~5.0.3",
"hawk": "~6.0.2",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
Expand Down
4 changes: 2 additions & 2 deletions request.js
Expand Up @@ -6,7 +6,6 @@ var url = require('url')
var util = require('util')
var stream = require('stream')
var zlib = require('zlib')
var hawk = require('hawk')
var aws2 = require('aws-sign2')
var aws4 = require('aws4')
var httpSignature = require('http-signature')
Expand All @@ -25,6 +24,7 @@ var Querystring = require('./lib/querystring').Querystring
var Har = require('./lib/har').Har
var Auth = require('./lib/auth').Auth
var OAuth = require('./lib/oauth').OAuth
var hawk = require('./lib/hawk')
var Multipart = require('./lib/multipart').Multipart
var Redirect = require('./lib/redirect').Redirect
var Tunnel = require('./lib/tunnel').Tunnel
Expand Down Expand Up @@ -1426,7 +1426,7 @@ Request.prototype.httpSignature = function (opts) {
}
Request.prototype.hawk = function (opts) {
var self = this
self.setHeader('Authorization', hawk.client.header(self.uri, self.method, opts).field)
self.setHeader('Authorization', hawk.header(self.uri, self.method, opts))
}
Request.prototype.oauth = function (_oauth) {
var self = this
Expand Down
178 changes: 155 additions & 23 deletions tests/test-hawk.js
Expand Up @@ -2,27 +2,15 @@

var http = require('http')
var request = require('../index')
var hawk = require('hawk')
var hawk = require('../lib/hawk')
var tape = require('tape')
var assert = require('assert')

var server = http.createServer(function (req, res) {
var getCred = function (id, callback) {
assert.equal(id, 'dh37fgj492je')
var credentials = {
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
algorithm: 'sha256',
user: 'Steve'
}
return callback(null, credentials)
}

hawk.server.authenticate(req, getCred, {}, function (err, credentials, attributes) {
res.writeHead(err ? 401 : 200, {
'Content-Type': 'text/plain'
})
res.end(err ? 'Shoosh!' : 'Hello ' + credentials.user)
res.writeHead(200, {
'Content-Type': 'text/plain'
})
res.end(authenticate(req))
})

tape('setup', function (t) {
Expand All @@ -32,18 +20,124 @@ tape('setup', function (t) {
})
})

tape('hawk', function (t) {
var creds = {
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
algorithm: 'sha256',
id: 'dh37fgj492je'
}
var creds = {
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
algorithm: 'sha256',
id: 'dh37fgj492je'
}

tape('hawk-get', function (t) {
request(server.url, {
hawk: { credentials: creds }
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'Hello Steve')
t.equal(body, 'OK')
t.end()
})
})

tape('hawk-post', function (t) {
request.post({ url: server.url, body: 'hello', hawk: { credentials: creds, payload: 'hello' } }, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'OK')
t.end()
})
})

tape('hawk-ext', function (t) {
request(server.url, {
hawk: { credentials: creds, ext: 'test' }
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'OK')
t.end()
})
})

tape('hawk-app', function (t) {
request(server.url, {
hawk: { credentials: creds, app: 'test' }
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'OK')
t.end()
})
})

tape('hawk-app+dlg', function (t) {
request(server.url, {
hawk: { credentials: creds, app: 'test', dlg: 'asd' }
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'OK')
t.end()
})
})

tape('hawk-missing-creds', function (t) {
request(server.url, {
hawk: {}
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'FAIL')
t.end()
})
})

tape('hawk-missing-creds-id', function (t) {
request(server.url, {
hawk: {
credentials: {}
}
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'FAIL')
t.end()
})
})

tape('hawk-missing-creds-key', function (t) {
request(server.url, {
hawk: {
credentials: { id: 'asd' }
}
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'FAIL')
t.end()
})
})

tape('hawk-missing-creds-algo', function (t) {
request(server.url, {
hawk: {
credentials: { key: '123', id: '123' }
}
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'FAIL')
t.end()
})
})

tape('hawk-invalid-creds-algo', function (t) {
request(server.url, {
hawk: {
credentials: { key: '123', id: '123', algorithm: 'xx' }
}
}, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.equal(body, 'FAIL')
t.end()
})
})
Expand All @@ -53,3 +147,41 @@ tape('cleanup', function (t) {
t.end()
})
})

function authenticate (req) {
if (!req.headers.authorization) {
return 'FAIL'
}

var headerParts = req.headers.authorization.match(/^(\w+)(?:\s+(.*))?$/)
assert.equal(headerParts[1], 'Hawk')
var attributes = {}
headerParts[2].replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) { attributes[$1] = $2 })
var hostParts = req.headers.host.split(':')

const artifacts = {
method: req.method,
host: hostParts[0],
port: (hostParts[1] ? hostParts[1] : (req.connection && req.connection.encrypted ? 443 : 80)),
resource: req.url,
ts: attributes.ts,
nonce: attributes.nonce,
hash: attributes.hash,
ext: attributes.ext,
app: attributes.app,
dlg: attributes.dlg,
mac: attributes.mac,
id: attributes.id
}

assert.equal(attributes.id, 'dh37fgj492je')
var credentials = {
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
algorithm: 'sha256',
user: 'Steve'
}

const mac = hawk.calculateMac(credentials, artifacts)
assert.equal(mac, attributes.mac)
return 'OK'
}