twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

parser.js (9841B)


      1 // Copyright 2012 Joyent, Inc.  All rights reserved.
      2 
      3 var assert = require('assert-plus');
      4 var util = require('util');
      5 var utils = require('./utils');
      6 
      7 
      8 
      9 ///--- Globals
     10 
     11 var HASH_ALGOS = utils.HASH_ALGOS;
     12 var PK_ALGOS = utils.PK_ALGOS;
     13 var HttpSignatureError = utils.HttpSignatureError;
     14 var InvalidAlgorithmError = utils.InvalidAlgorithmError;
     15 var validateAlgorithm = utils.validateAlgorithm;
     16 
     17 var State = {
     18   New: 0,
     19   Params: 1
     20 };
     21 
     22 var ParamsState = {
     23   Name: 0,
     24   Quote: 1,
     25   Value: 2,
     26   Comma: 3
     27 };
     28 
     29 
     30 ///--- Specific Errors
     31 
     32 
     33 function ExpiredRequestError(message) {
     34   HttpSignatureError.call(this, message, ExpiredRequestError);
     35 }
     36 util.inherits(ExpiredRequestError, HttpSignatureError);
     37 
     38 
     39 function InvalidHeaderError(message) {
     40   HttpSignatureError.call(this, message, InvalidHeaderError);
     41 }
     42 util.inherits(InvalidHeaderError, HttpSignatureError);
     43 
     44 
     45 function InvalidParamsError(message) {
     46   HttpSignatureError.call(this, message, InvalidParamsError);
     47 }
     48 util.inherits(InvalidParamsError, HttpSignatureError);
     49 
     50 
     51 function MissingHeaderError(message) {
     52   HttpSignatureError.call(this, message, MissingHeaderError);
     53 }
     54 util.inherits(MissingHeaderError, HttpSignatureError);
     55 
     56 function StrictParsingError(message) {
     57   HttpSignatureError.call(this, message, StrictParsingError);
     58 }
     59 util.inherits(StrictParsingError, HttpSignatureError);
     60 
     61 ///--- Exported API
     62 
     63 module.exports = {
     64 
     65   /**
     66    * Parses the 'Authorization' header out of an http.ServerRequest object.
     67    *
     68    * Note that this API will fully validate the Authorization header, and throw
     69    * on any error.  It will not however check the signature, or the keyId format
     70    * as those are specific to your environment.  You can use the options object
     71    * to pass in extra constraints.
     72    *
     73    * As a response object you can expect this:
     74    *
     75    *     {
     76    *       "scheme": "Signature",
     77    *       "params": {
     78    *         "keyId": "foo",
     79    *         "algorithm": "rsa-sha256",
     80    *         "headers": [
     81    *           "date" or "x-date",
     82    *           "digest"
     83    *         ],
     84    *         "signature": "base64"
     85    *       },
     86    *       "signingString": "ready to be passed to crypto.verify()"
     87    *     }
     88    *
     89    * @param {Object} request an http.ServerRequest.
     90    * @param {Object} options an optional options object with:
     91    *                   - clockSkew: allowed clock skew in seconds (default 300).
     92    *                   - headers: required header names (def: date or x-date)
     93    *                   - algorithms: algorithms to support (default: all).
     94    *                   - strict: should enforce latest spec parsing
     95    *                             (default: false).
     96    * @return {Object} parsed out object (see above).
     97    * @throws {TypeError} on invalid input.
     98    * @throws {InvalidHeaderError} on an invalid Authorization header error.
     99    * @throws {InvalidParamsError} if the params in the scheme are invalid.
    100    * @throws {MissingHeaderError} if the params indicate a header not present,
    101    *                              either in the request headers from the params,
    102    *                              or not in the params from a required header
    103    *                              in options.
    104    * @throws {StrictParsingError} if old attributes are used in strict parsing
    105    *                              mode.
    106    * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew.
    107    */
    108   parseRequest: function parseRequest(request, options) {
    109     assert.object(request, 'request');
    110     assert.object(request.headers, 'request.headers');
    111     if (options === undefined) {
    112       options = {};
    113     }
    114     if (options.headers === undefined) {
    115       options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
    116     }
    117     assert.object(options, 'options');
    118     assert.arrayOfString(options.headers, 'options.headers');
    119     assert.optionalFinite(options.clockSkew, 'options.clockSkew');
    120 
    121     var authzHeaderName = options.authorizationHeaderName || 'authorization';
    122 
    123     if (!request.headers[authzHeaderName]) {
    124       throw new MissingHeaderError('no ' + authzHeaderName + ' header ' +
    125                                    'present in the request');
    126     }
    127 
    128     options.clockSkew = options.clockSkew || 300;
    129 
    130 
    131     var i = 0;
    132     var state = State.New;
    133     var substate = ParamsState.Name;
    134     var tmpName = '';
    135     var tmpValue = '';
    136 
    137     var parsed = {
    138       scheme: '',
    139       params: {},
    140       signingString: ''
    141     };
    142 
    143     var authz = request.headers[authzHeaderName];
    144     for (i = 0; i < authz.length; i++) {
    145       var c = authz.charAt(i);
    146 
    147       switch (Number(state)) {
    148 
    149       case State.New:
    150         if (c !== ' ') parsed.scheme += c;
    151         else state = State.Params;
    152         break;
    153 
    154       case State.Params:
    155         switch (Number(substate)) {
    156 
    157         case ParamsState.Name:
    158           var code = c.charCodeAt(0);
    159           // restricted name of A-Z / a-z
    160           if ((code >= 0x41 && code <= 0x5a) || // A-Z
    161               (code >= 0x61 && code <= 0x7a)) { // a-z
    162             tmpName += c;
    163           } else if (c === '=') {
    164             if (tmpName.length === 0)
    165               throw new InvalidHeaderError('bad param format');
    166             substate = ParamsState.Quote;
    167           } else {
    168             throw new InvalidHeaderError('bad param format');
    169           }
    170           break;
    171 
    172         case ParamsState.Quote:
    173           if (c === '"') {
    174             tmpValue = '';
    175             substate = ParamsState.Value;
    176           } else {
    177             throw new InvalidHeaderError('bad param format');
    178           }
    179           break;
    180 
    181         case ParamsState.Value:
    182           if (c === '"') {
    183             parsed.params[tmpName] = tmpValue;
    184             substate = ParamsState.Comma;
    185           } else {
    186             tmpValue += c;
    187           }
    188           break;
    189 
    190         case ParamsState.Comma:
    191           if (c === ',') {
    192             tmpName = '';
    193             substate = ParamsState.Name;
    194           } else {
    195             throw new InvalidHeaderError('bad param format');
    196           }
    197           break;
    198 
    199         default:
    200           throw new Error('Invalid substate');
    201         }
    202         break;
    203 
    204       default:
    205         throw new Error('Invalid substate');
    206       }
    207 
    208     }
    209 
    210     if (!parsed.params.headers || parsed.params.headers === '') {
    211       if (request.headers['x-date']) {
    212         parsed.params.headers = ['x-date'];
    213       } else {
    214         parsed.params.headers = ['date'];
    215       }
    216     } else {
    217       parsed.params.headers = parsed.params.headers.split(' ');
    218     }
    219 
    220     // Minimally validate the parsed object
    221     if (!parsed.scheme || parsed.scheme !== 'Signature')
    222       throw new InvalidHeaderError('scheme was not "Signature"');
    223 
    224     if (!parsed.params.keyId)
    225       throw new InvalidHeaderError('keyId was not specified');
    226 
    227     if (!parsed.params.algorithm)
    228       throw new InvalidHeaderError('algorithm was not specified');
    229 
    230     if (!parsed.params.signature)
    231       throw new InvalidHeaderError('signature was not specified');
    232 
    233     // Check the algorithm against the official list
    234     parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
    235     try {
    236       validateAlgorithm(parsed.params.algorithm);
    237     } catch (e) {
    238       if (e instanceof InvalidAlgorithmError)
    239         throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' +
    240           'supported'));
    241       else
    242         throw (e);
    243     }
    244 
    245     // Build the signingString
    246     for (i = 0; i < parsed.params.headers.length; i++) {
    247       var h = parsed.params.headers[i].toLowerCase();
    248       parsed.params.headers[i] = h;
    249 
    250       if (h === 'request-line') {
    251         if (!options.strict) {
    252           /*
    253            * We allow headers from the older spec drafts if strict parsing isn't
    254            * specified in options.
    255            */
    256           parsed.signingString +=
    257             request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
    258         } else {
    259           /* Strict parsing doesn't allow older draft headers. */
    260           throw (new StrictParsingError('request-line is not a valid header ' +
    261             'with strict parsing enabled.'));
    262         }
    263       } else if (h === '(request-target)') {
    264         parsed.signingString +=
    265           '(request-target): ' + request.method.toLowerCase() + ' ' +
    266           request.url;
    267       } else {
    268         var value = request.headers[h];
    269         if (value === undefined)
    270           throw new MissingHeaderError(h + ' was not in the request');
    271         parsed.signingString += h + ': ' + value;
    272       }
    273 
    274       if ((i + 1) < parsed.params.headers.length)
    275         parsed.signingString += '\n';
    276     }
    277 
    278     // Check against the constraints
    279     var date;
    280     if (request.headers.date || request.headers['x-date']) {
    281         if (request.headers['x-date']) {
    282           date = new Date(request.headers['x-date']);
    283         } else {
    284           date = new Date(request.headers.date);
    285         }
    286       var now = new Date();
    287       var skew = Math.abs(now.getTime() - date.getTime());
    288 
    289       if (skew > options.clockSkew * 1000) {
    290         throw new ExpiredRequestError('clock skew of ' +
    291                                       (skew / 1000) +
    292                                       's was greater than ' +
    293                                       options.clockSkew + 's');
    294       }
    295     }
    296 
    297     options.headers.forEach(function (hdr) {
    298       // Remember that we already checked any headers in the params
    299       // were in the request, so if this passes we're good.
    300       if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0)
    301         throw new MissingHeaderError(hdr + ' was not a signed header');
    302     });
    303 
    304     if (options.algorithms) {
    305       if (options.algorithms.indexOf(parsed.params.algorithm) === -1)
    306         throw new InvalidParamsError(parsed.params.algorithm +
    307                                      ' is not a supported algorithm');
    308     }
    309 
    310     parsed.algorithm = parsed.params.algorithm.toUpperCase();
    311     parsed.keyId = parsed.params.keyId;
    312     return parsed;
    313   }
    314 
    315 };