Initial commit
[yaffs-website] / node_modules / http-signature / lib / parser.js
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.optionalNumber(options.clockSkew, 'options.clockSkew');
120
121     if (!request.headers.authorization)
122       throw new MissingHeaderError('no authorization header present in ' +
123                                    'the request');
124
125     options.clockSkew = options.clockSkew || 300;
126
127
128     var i = 0;
129     var state = State.New;
130     var substate = ParamsState.Name;
131     var tmpName = '';
132     var tmpValue = '';
133
134     var parsed = {
135       scheme: '',
136       params: {},
137       signingString: '',
138
139       get algorithm() {
140         return this.params.algorithm.toUpperCase();
141       },
142
143       get keyId() {
144         return this.params.keyId;
145       }
146     };
147
148     var authz = request.headers.authorization;
149     for (i = 0; i < authz.length; i++) {
150       var c = authz.charAt(i);
151
152       switch (Number(state)) {
153
154       case State.New:
155         if (c !== ' ') parsed.scheme += c;
156         else state = State.Params;
157         break;
158
159       case State.Params:
160         switch (Number(substate)) {
161
162         case ParamsState.Name:
163           var code = c.charCodeAt(0);
164           // restricted name of A-Z / a-z
165           if ((code >= 0x41 && code <= 0x5a) || // A-Z
166               (code >= 0x61 && code <= 0x7a)) { // a-z
167             tmpName += c;
168           } else if (c === '=') {
169             if (tmpName.length === 0)
170               throw new InvalidHeaderError('bad param format');
171             substate = ParamsState.Quote;
172           } else {
173             throw new InvalidHeaderError('bad param format');
174           }
175           break;
176
177         case ParamsState.Quote:
178           if (c === '"') {
179             tmpValue = '';
180             substate = ParamsState.Value;
181           } else {
182             throw new InvalidHeaderError('bad param format');
183           }
184           break;
185
186         case ParamsState.Value:
187           if (c === '"') {
188             parsed.params[tmpName] = tmpValue;
189             substate = ParamsState.Comma;
190           } else {
191             tmpValue += c;
192           }
193           break;
194
195         case ParamsState.Comma:
196           if (c === ',') {
197             tmpName = '';
198             substate = ParamsState.Name;
199           } else {
200             throw new InvalidHeaderError('bad param format');
201           }
202           break;
203
204         default:
205           throw new Error('Invalid substate');
206         }
207         break;
208
209       default:
210         throw new Error('Invalid substate');
211       }
212
213     }
214
215     if (!parsed.params.headers || parsed.params.headers === '') {
216       if (request.headers['x-date']) {
217         parsed.params.headers = ['x-date'];
218       } else {
219         parsed.params.headers = ['date'];
220       }
221     } else {
222       parsed.params.headers = parsed.params.headers.split(' ');
223     }
224
225     // Minimally validate the parsed object
226     if (!parsed.scheme || parsed.scheme !== 'Signature')
227       throw new InvalidHeaderError('scheme was not "Signature"');
228
229     if (!parsed.params.keyId)
230       throw new InvalidHeaderError('keyId was not specified');
231
232     if (!parsed.params.algorithm)
233       throw new InvalidHeaderError('algorithm was not specified');
234
235     if (!parsed.params.signature)
236       throw new InvalidHeaderError('signature was not specified');
237
238     // Check the algorithm against the official list
239     parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
240     try {
241       validateAlgorithm(parsed.params.algorithm);
242     } catch (e) {
243       if (e instanceof InvalidAlgorithmError)
244         throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' +
245           'supported'));
246       else
247         throw (e);
248     }
249
250     // Build the signingString
251     for (i = 0; i < parsed.params.headers.length; i++) {
252       var h = parsed.params.headers[i].toLowerCase();
253       parsed.params.headers[i] = h;
254
255       if (h === 'request-line') {
256         if (!options.strict) {
257           /*
258            * We allow headers from the older spec drafts if strict parsing isn't
259            * specified in options.
260            */
261           parsed.signingString +=
262             request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
263         } else {
264           /* Strict parsing doesn't allow older draft headers. */
265           throw (new StrictParsingError('request-line is not a valid header ' +
266             'with strict parsing enabled.'));
267         }
268       } else if (h === '(request-target)') {
269         parsed.signingString +=
270           '(request-target): ' + request.method.toLowerCase() + ' ' +
271           request.url;
272       } else {
273         var value = request.headers[h];
274         if (value === undefined)
275           throw new MissingHeaderError(h + ' was not in the request');
276         parsed.signingString += h + ': ' + value;
277       }
278
279       if ((i + 1) < parsed.params.headers.length)
280         parsed.signingString += '\n';
281     }
282
283     // Check against the constraints
284     var date;
285     if (request.headers.date || request.headers['x-date']) {
286         if (request.headers['x-date']) {
287           date = new Date(request.headers['x-date']);
288         } else {
289           date = new Date(request.headers.date);
290         }
291       var now = new Date();
292       var skew = Math.abs(now.getTime() - date.getTime());
293
294       if (skew > options.clockSkew * 1000) {
295         throw new ExpiredRequestError('clock skew of ' +
296                                       (skew / 1000) +
297                                       's was greater than ' +
298                                       options.clockSkew + 's');
299       }
300     }
301
302     options.headers.forEach(function (hdr) {
303       // Remember that we already checked any headers in the params
304       // were in the request, so if this passes we're good.
305       if (parsed.params.headers.indexOf(hdr) < 0)
306         throw new MissingHeaderError(hdr + ' was not a signed header');
307     });
308
309     if (options.algorithms) {
310       if (options.algorithms.indexOf(parsed.params.algorithm) === -1)
311         throw new InvalidParamsError(parsed.params.algorithm +
312                                      ' is not a supported algorithm');
313     }
314
315     return parsed;
316   }
317
318 };