parser.js 9.5 KB

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