Spaces:
Configuration error
Configuration error
// Copyright 2012 Joyent, Inc. All rights reserved. | |
var assert = require('assert-plus'); | |
var util = require('util'); | |
var utils = require('./utils'); | |
///--- Globals | |
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 | |
}; | |
///--- Specific Errors | |
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); | |
///--- Exported API | |
module.exports = { | |
/** | |
* Parses the 'Authorization' header out of an http.ServerRequest object. | |
* | |
* Note that this API will fully validate the Authorization header, and throw | |
* on any error. It will not however check the signature, or the keyId format | |
* as those are specific to your environment. You can use the options object | |
* to pass in extra constraints. | |
* | |
* As a response object you can expect this: | |
* | |
* { | |
* "scheme": "Signature", | |
* "params": { | |
* "keyId": "foo", | |
* "algorithm": "rsa-sha256", | |
* "headers": [ | |
* "date" or "x-date", | |
* "digest" | |
* ], | |
* "signature": "base64" | |
* }, | |
* "signingString": "ready to be passed to crypto.verify()" | |
* } | |
* | |
* @param {Object} request an http.ServerRequest. | |
* @param {Object} options an optional options object with: | |
* - clockSkew: allowed clock skew in seconds (default 300). | |
* - headers: required header names (def: date or x-date) | |
* - algorithms: algorithms to support (default: all). | |
* - strict: should enforce latest spec parsing | |
* (default: false). | |
* @return {Object} parsed out object (see above). | |
* @throws {TypeError} on invalid input. | |
* @throws {InvalidHeaderError} on an invalid Authorization header error. | |
* @throws {InvalidParamsError} if the params in the scheme are invalid. | |
* @throws {MissingHeaderError} if the params indicate a header not present, | |
* either in the request headers from the params, | |
* or not in the params from a required header | |
* in options. | |
* @throws {StrictParsingError} if old attributes are used in strict parsing | |
* mode. | |
* @throws {ExpiredRequestError} if the value of date or x-date exceeds skew. | |
*/ | |
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); | |
// restricted name of A-Z / a-z | |
if ((code >= 0x41 && code <= 0x5a) || // A-Z | |
(code >= 0x61 && code <= 0x7a)) { // a-z | |
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 { | |
//number | |
substate = ParamsState.Number; | |
code = c.charCodeAt(0); | |
if (code < 0x30 || code > 0x39) { //character not in 0-9 | |
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) { //character not in 0-9 | |
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(' '); | |
} | |
// Minimally validate the parsed object | |
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'); | |
// Check the algorithm against the official list | |
try { | |
validateAlgorithm(parsed.params.algorithm); | |
} catch (e) { | |
if (e instanceof InvalidAlgorithmError) | |
throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' + | |
'supported')); | |
else | |
throw (e); | |
} | |
// Build the signingString | |
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) { | |
/* | |
* We allow headers from the older spec drafts if strict parsing isn't | |
* specified in options. | |
*/ | |
parsed.signingString += | |
request.method + ' ' + request.url + ' HTTP/' + request.httpVersion; | |
} else { | |
/* Strict parsing doesn't allow older draft headers. */ | |
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'; | |
} | |
// Check against the constraints | |
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) { | |
// Remember that we already checked any headers in the params | |
// were in the request, so if this passes we're good. | |
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; | |
} | |
}; | |