// 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
|
|
};
|
|
|
|
|
|
///--- 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 = {};
|
|
}
|
|
if (options.headers === undefined) {
|
|
options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
|
|
}
|
|
assert.object(options, 'options');
|
|
assert.arrayOfString(options.headers, 'options.headers');
|
|
assert.optionalFinite(options.clockSkew, 'options.clockSkew');
|
|
|
|
var authzHeaderName = options.authorizationHeaderName || 'authorization';
|
|
|
|
if (!request.headers[authzHeaderName]) {
|
|
throw new MissingHeaderError('no ' + authzHeaderName + ' header ' +
|
|
'present in the request');
|
|
}
|
|
|
|
options.clockSkew = options.clockSkew || 300;
|
|
|
|
|
|
var i = 0;
|
|
var state = State.New;
|
|
var substate = ParamsState.Name;
|
|
var tmpName = '';
|
|
var tmpValue = '';
|
|
|
|
var parsed = {
|
|
scheme: '',
|
|
params: {},
|
|
signingString: ''
|
|
};
|
|
|
|
var authz = request.headers[authzHeaderName];
|
|
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 {
|
|
throw new InvalidHeaderError('bad param format');
|
|
}
|
|
break;
|
|
|
|
case ParamsState.Value:
|
|
if (c === '"') {
|
|
parsed.params[tmpName] = tmpValue;
|
|
substate = ParamsState.Comma;
|
|
} else {
|
|
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
|
|
parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
|
|
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 {
|
|
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;
|
|
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();
|
|
var 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');
|
|
}
|
|
}
|
|
|
|
options.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');
|
|
});
|
|
|
|
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;
|
|
return parsed;
|
|
}
|
|
|
|
};
|