123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- // 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.optionalNumber(options.clockSkew, 'options.clockSkew');
-
- if (!request.headers.authorization)
- throw new MissingHeaderError('no authorization 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: '',
-
- get algorithm() {
- return this.params.algorithm.toUpperCase();
- },
-
- get keyId() {
- return this.params.keyId;
- }
- };
-
- var authz = request.headers.authorization;
- 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) < 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');
- }
-
- return parsed;
- }
-
- };
|