parser.js 8.7 KB

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