|
|
|
|
|
var assert = require('assert-plus'); |
|
var util = require('util'); |
|
var utils = require('./utils'); |
|
|
|
|
|
|
|
|
|
|
|
var HASH_ALGOS = utils.HASH_ALGOS; |
|
var PK_ALGOS = utils.PK_ALGOS; |
|
var HttpSignatureError = utils.HttpSignatureError; |
|
var InvalidAlgorithmError = utils.InvalidAlgorithmError; |
|
var validateAlgorithm = utils.validateAlgorithm; |
|
|
|
var State = { |
|
New: 0, |
|
Params: 1 |
|
}; |
|
|
|
var ParamsState = { |
|
Name: 0, |
|
Quote: 1, |
|
Value: 2, |
|
Comma: 3, |
|
Number: 4 |
|
}; |
|
|
|
|
|
|
|
|
|
function ExpiredRequestError(message) { |
|
HttpSignatureError.call(this, message, ExpiredRequestError); |
|
} |
|
util.inherits(ExpiredRequestError, HttpSignatureError); |
|
|
|
|
|
function InvalidHeaderError(message) { |
|
HttpSignatureError.call(this, message, InvalidHeaderError); |
|
} |
|
util.inherits(InvalidHeaderError, HttpSignatureError); |
|
|
|
|
|
function InvalidParamsError(message) { |
|
HttpSignatureError.call(this, message, InvalidParamsError); |
|
} |
|
util.inherits(InvalidParamsError, HttpSignatureError); |
|
|
|
|
|
function MissingHeaderError(message) { |
|
HttpSignatureError.call(this, message, MissingHeaderError); |
|
} |
|
util.inherits(MissingHeaderError, HttpSignatureError); |
|
|
|
function StrictParsingError(message) { |
|
HttpSignatureError.call(this, message, StrictParsingError); |
|
} |
|
util.inherits(StrictParsingError, HttpSignatureError); |
|
|
|
|
|
|
|
module.exports = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseRequest: function parseRequest(request, options) { |
|
assert.object(request, 'request'); |
|
assert.object(request.headers, 'request.headers'); |
|
if (options === undefined) { |
|
options = {}; |
|
} |
|
assert.object(options, 'options'); |
|
assert.optionalFinite(options.clockSkew, 'options.clockSkew'); |
|
|
|
var headers = [request.headers['x-date'] ? 'x-date' : 'date']; |
|
if (options.headers !== undefined) { |
|
assert.arrayOfString(headers, 'options.headers'); |
|
headers = options.headers; |
|
} |
|
|
|
var authzHeaderName = options.authorizationHeaderName; |
|
var authz = request.headers[authzHeaderName] || |
|
request.headers[utils.HEADER.AUTH] || request.headers[utils.HEADER.SIG]; |
|
|
|
if (!authz) { |
|
var errHeader = authzHeaderName ? authzHeaderName : |
|
utils.HEADER.AUTH + ' or ' + utils.HEADER.SIG; |
|
|
|
throw new MissingHeaderError('no ' + errHeader + ' header ' + |
|
'present in the request'); |
|
} |
|
|
|
options.clockSkew = options.clockSkew || 300; |
|
|
|
|
|
var i = 0; |
|
var state = authz === request.headers[utils.HEADER.SIG] ? |
|
State.Params : State.New; |
|
var substate = ParamsState.Name; |
|
var tmpName = ''; |
|
var tmpValue = ''; |
|
|
|
var parsed = { |
|
scheme: authz === request.headers[utils.HEADER.SIG] ? 'Signature' : '', |
|
params: {}, |
|
signingString: '' |
|
}; |
|
|
|
for (i = 0; i < authz.length; i++) { |
|
var c = authz.charAt(i); |
|
|
|
switch (Number(state)) { |
|
|
|
case State.New: |
|
if (c !== ' ') parsed.scheme += c; |
|
else state = State.Params; |
|
break; |
|
|
|
case State.Params: |
|
switch (Number(substate)) { |
|
|
|
case ParamsState.Name: |
|
var code = c.charCodeAt(0); |
|
|
|
if ((code >= 0x41 && code <= 0x5a) || |
|
(code >= 0x61 && code <= 0x7a)) { |
|
tmpName += c; |
|
} else if (c === '=') { |
|
if (tmpName.length === 0) |
|
throw new InvalidHeaderError('bad param format'); |
|
substate = ParamsState.Quote; |
|
} else { |
|
throw new InvalidHeaderError('bad param format'); |
|
} |
|
break; |
|
|
|
case ParamsState.Quote: |
|
if (c === '"') { |
|
tmpValue = ''; |
|
substate = ParamsState.Value; |
|
} else { |
|
|
|
substate = ParamsState.Number; |
|
code = c.charCodeAt(0); |
|
if (code < 0x30 || code > 0x39) { |
|
throw new InvalidHeaderError('bad param format'); |
|
} |
|
tmpValue = c; |
|
} |
|
break; |
|
|
|
case ParamsState.Value: |
|
if (c === '"') { |
|
parsed.params[tmpName] = tmpValue; |
|
substate = ParamsState.Comma; |
|
} else { |
|
tmpValue += c; |
|
} |
|
break; |
|
|
|
case ParamsState.Number: |
|
if (c === ',') { |
|
parsed.params[tmpName] = parseInt(tmpValue, 10); |
|
tmpName = ''; |
|
substate = ParamsState.Name; |
|
} else { |
|
code = c.charCodeAt(0); |
|
if (code < 0x30 || code > 0x39) { |
|
throw new InvalidHeaderError('bad param format'); |
|
} |
|
tmpValue += c; |
|
} |
|
break; |
|
|
|
|
|
case ParamsState.Comma: |
|
if (c === ',') { |
|
tmpName = ''; |
|
substate = ParamsState.Name; |
|
} else { |
|
throw new InvalidHeaderError('bad param format'); |
|
} |
|
break; |
|
|
|
default: |
|
throw new Error('Invalid substate'); |
|
} |
|
break; |
|
|
|
default: |
|
throw new Error('Invalid substate'); |
|
} |
|
|
|
} |
|
|
|
if (!parsed.params.headers || parsed.params.headers === '') { |
|
if (request.headers['x-date']) { |
|
parsed.params.headers = ['x-date']; |
|
} else { |
|
parsed.params.headers = ['date']; |
|
} |
|
} else { |
|
parsed.params.headers = parsed.params.headers.split(' '); |
|
} |
|
|
|
|
|
if (!parsed.scheme || parsed.scheme !== 'Signature') |
|
throw new InvalidHeaderError('scheme was not "Signature"'); |
|
|
|
if (!parsed.params.keyId) |
|
throw new InvalidHeaderError('keyId was not specified'); |
|
|
|
if (!parsed.params.algorithm) |
|
throw new InvalidHeaderError('algorithm was not specified'); |
|
|
|
if (!parsed.params.signature) |
|
throw new InvalidHeaderError('signature was not specified'); |
|
|
|
|
|
try { |
|
validateAlgorithm(parsed.params.algorithm); |
|
} catch (e) { |
|
if (e instanceof InvalidAlgorithmError) |
|
throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' + |
|
'supported')); |
|
else |
|
throw (e); |
|
} |
|
|
|
|
|
for (i = 0; i < parsed.params.headers.length; i++) { |
|
var h = parsed.params.headers[i].toLowerCase(); |
|
parsed.params.headers[i] = h; |
|
|
|
if (h === 'request-line') { |
|
if (!options.strict) { |
|
|
|
|
|
|
|
|
|
parsed.signingString += |
|
request.method + ' ' + request.url + ' HTTP/' + request.httpVersion; |
|
} else { |
|
|
|
throw (new StrictParsingError('request-line is not a valid header ' + |
|
'with strict parsing enabled.')); |
|
} |
|
} else if (h === '(request-target)') { |
|
parsed.signingString += |
|
'(request-target): ' + request.method.toLowerCase() + ' ' + |
|
request.url; |
|
} else if (h === '(keyid)') { |
|
parsed.signingString += '(keyid): ' + parsed.params.keyId; |
|
} else if (h === '(algorithm)') { |
|
parsed.signingString += '(algorithm): ' + parsed.params.algorithm; |
|
} else if (h === '(opaque)') { |
|
var opaque = parsed.params.opaque; |
|
if (opaque === undefined) { |
|
throw new MissingHeaderError('opaque param was not in the ' + |
|
authzHeaderName + ' header'); |
|
} |
|
parsed.signingString += '(opaque): ' + opaque; |
|
} else if (h === '(created)') { |
|
parsed.signingString += '(created): ' + parsed.params.created; |
|
} else if (h === '(expires)') { |
|
parsed.signingString += '(expires): ' + parsed.params.expires; |
|
} else { |
|
var value = request.headers[h]; |
|
if (value === undefined) |
|
throw new MissingHeaderError(h + ' was not in the request'); |
|
parsed.signingString += h + ': ' + value; |
|
} |
|
|
|
if ((i + 1) < parsed.params.headers.length) |
|
parsed.signingString += '\n'; |
|
} |
|
|
|
|
|
var date; |
|
var skew; |
|
if (request.headers.date || request.headers['x-date']) { |
|
if (request.headers['x-date']) { |
|
date = new Date(request.headers['x-date']); |
|
} else { |
|
date = new Date(request.headers.date); |
|
} |
|
var now = new Date(); |
|
skew = Math.abs(now.getTime() - date.getTime()); |
|
|
|
if (skew > options.clockSkew * 1000) { |
|
throw new ExpiredRequestError('clock skew of ' + |
|
(skew / 1000) + |
|
's was greater than ' + |
|
options.clockSkew + 's'); |
|
} |
|
} |
|
|
|
if (parsed.params.created) { |
|
skew = parsed.params.created - Math.floor(Date.now() / 1000); |
|
if (skew > options.clockSkew) { |
|
throw new ExpiredRequestError('Created lies in the future (with ' + |
|
'skew ' + skew + 's greater than allowed ' + options.clockSkew + |
|
's'); |
|
} |
|
} |
|
|
|
if (parsed.params.expires) { |
|
var expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires; |
|
if (expiredSince > options.clockSkew) { |
|
throw new ExpiredRequestError('Request expired with skew ' + |
|
expiredSince + 's greater than allowed ' + options.clockSkew + 's'); |
|
} |
|
} |
|
|
|
headers.forEach(function (hdr) { |
|
|
|
|
|
if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0) |
|
throw new MissingHeaderError(hdr + ' was not a signed header'); |
|
}); |
|
|
|
parsed.params.algorithm = parsed.params.algorithm.toLowerCase(); |
|
if (options.algorithms) { |
|
if (options.algorithms.indexOf(parsed.params.algorithm) === -1) |
|
throw new InvalidParamsError(parsed.params.algorithm + |
|
' is not a supported algorithm'); |
|
} |
|
|
|
parsed.algorithm = parsed.params.algorithm.toUpperCase(); |
|
parsed.keyId = parsed.params.keyId; |
|
parsed.opaque = parsed.params.opaque; |
|
return parsed; |
|
} |
|
|
|
}; |
|
|