| // 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; |
| } |
| |
| }; |