Advertisement
Guest User

Untitled

a guest
Apr 21st, 2015
224
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.05 KB | None | 0 0
  1. // Copyright 2012 Mark Cavage, Inc. All rights reserved.
  2.  
  3. var EventEmitter = require('events').EventEmitter;
  4. var url = require('url');
  5. var util = require('util');
  6.  
  7. var assert = require('assert-plus');
  8. var deepEqual = require('deep-equal');
  9. var LRU = require('lru-cache');
  10. var Negotiator = require('negotiator');
  11. var semver = require('semver');
  12.  
  13. var cors = require('./plugins/cors');
  14. var errors = require('./errors');
  15. var utils = require('./utils');
  16.  
  17.  
  18. ///--- Globals
  19.  
  20. var DEF_CT = 'application/octet-stream';
  21.  
  22. var maxSatisfying = semver.maxSatisfying;
  23.  
  24. var BadRequestError = errors.BadRequestError;
  25. var InternalError = errors.InternalError;
  26. var InvalidArgumentError = errors.InvalidArgumentError;
  27. var InvalidVersionError = errors.InvalidVersionError;
  28. var MethodNotAllowedError = errors.MethodNotAllowedError;
  29. var ResourceNotFoundError = errors.ResourceNotFoundError;
  30. var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
  31.  
  32. var shallowCopy = utils.shallowCopy;
  33.  
  34.  
  35. ///--- Helpers
  36.  
  37. function createCachedRoute(o, path, version, route) {
  38. if (!o.hasOwnProperty(path))
  39. o[path] = {};
  40.  
  41. if (!o[path].hasOwnProperty(version))
  42. o[path][version] = route;
  43. }
  44.  
  45.  
  46. function matchURL(re, req) {
  47. var i = 0;
  48. var result = re.exec(req.path());
  49. var params = {};
  50.  
  51. if (!result)
  52. return (false);
  53.  
  54. // This means the user original specified a regexp match, not a url
  55. // string like /:foo/:bar
  56. if (!re.restifyParams) {
  57. for (i = 1; i < result.length; i++)
  58. params[(i - 1)] = result[i];
  59.  
  60. return (params);
  61. }
  62.  
  63. // This was a static string, like /foo
  64. if (re.restifyParams.length === 0)
  65. return (params);
  66.  
  67. // This was the "normal" case, of /foo/:id
  68. re.restifyParams.forEach(function (p) {
  69. if (++i < result.length)
  70. params[p] = decodeURIComponent(result[i]);
  71. });
  72.  
  73. return (params);
  74. }
  75.  
  76.  
  77. function compileURL(options) {
  78. if (options.url instanceof RegExp)
  79. return (options.url);
  80. assert.string(options.url, 'url');
  81.  
  82. var params = [];
  83. var pattern = '^';
  84. var re;
  85. var _url = url.parse(options.url).pathname;
  86. _url.split('/').forEach(function (frag) {
  87. if (frag.length <= 0)
  88. return (false);
  89.  
  90. pattern += '\\/+';
  91. if (frag.charAt(0) === ':') {
  92. if (options.urlParamPattern) {
  93. pattern += '(' + options.urlParamPattern + ')';
  94. } else {
  95. pattern += '([^/]*)';
  96. }
  97. params.push(frag.slice(1));
  98. } else {
  99. pattern += frag;
  100. }
  101.  
  102. return (true);
  103. });
  104.  
  105. if (pattern === '^')
  106. pattern += '\\/';
  107. pattern += '$';
  108.  
  109. re = new RegExp(pattern, options.flags);
  110. re.restifyParams = params;
  111.  
  112. return (re);
  113. }
  114.  
  115.  
  116. ///--- API
  117.  
  118. function Router(options) {
  119. assert.object(options, 'options');
  120. assert.object(options.log, 'options.log');
  121.  
  122. EventEmitter.call(this);
  123.  
  124. this.cache = LRU({max: 100});
  125. this.contentType = options.contentType || [];
  126. if (!Array.isArray(this.contentType))
  127. this.contentType = [this.contentType];
  128. assert.arrayOfString(this.contentType, 'options.contentType');
  129.  
  130. this.log = options.log;
  131. this.mounts = {};
  132. this.name = 'RestifyRouter';
  133.  
  134. // A list of methods to routes
  135. this.routes = {
  136. DELETE: [],
  137. GET: [],
  138. HEAD: [],
  139. OPTIONS: [],
  140. PATCH: [],
  141. POST: [],
  142. PUT: []
  143. };
  144.  
  145. // So we can retrun 405 vs 404, we maintain a reverse mapping of URLs
  146. // to method
  147. this.reverse = {};
  148.  
  149. this.versions = options.versions || options.version || [];
  150. if (!Array.isArray(this.versions))
  151. this.versions = [this.versions];
  152. assert.arrayOfString(this.versions, 'options.versions');
  153.  
  154. this.versions.forEach(function (v) {
  155. if (semver.valid(v))
  156. return (true);
  157.  
  158. throw new InvalidArgumentError('%s is not a valid semver', v);
  159. });
  160. this.versions.sort();
  161.  
  162. }
  163. util.inherits(Router, EventEmitter);
  164. module.exports = Router;
  165.  
  166.  
  167. Router.prototype.render = function render(routeName, params, query) {
  168. function pathItem(match, key) {
  169. if (params.hasOwnProperty(key) === false) {
  170. throw new Error('Route <' + routeName +
  171. '> is missing parameter <' +
  172. key + '>');
  173. }
  174.  
  175. return ('/' + encodeURIComponent(params[key]));
  176. }
  177.  
  178. function queryItem(key) {
  179. return (encodeURIComponent(key) + '=' + encodeURIComponent(query[key]));
  180. }
  181.  
  182. var routeKey = routeName.replace(/\W/g, '').toLowerCase();
  183. var route = this.mounts[routeKey];
  184. if (!route)
  185. return (null);
  186.  
  187. var _url = route.spec.path.replace(/\/:([^/]+)/g, pathItem);
  188. var items = Object.keys(query || {}).map(queryItem);
  189. var queryString = items.length > 0 ? ('?' + items.join('&')) : '';
  190.  
  191. return (_url + queryString);
  192. };
  193.  
  194.  
  195. Router.prototype.mount = function mount(options) {
  196. assert.object(options, 'options');
  197. assert.string(options.method, 'options.method');
  198. assert.string(options.name, 'options.name');
  199.  
  200. var exists;
  201. var name = options.name;
  202. var route;
  203. var routes = this.routes[options.method];
  204. var self = this;
  205. var type = options.contentType || self.contentType;
  206. var versions = options.versions || options.version || self.versions;
  207.  
  208. if (type) {
  209. if (!Array.isArray(type))
  210. type = [type];
  211. type.filter(function (t) {
  212. return (t);
  213. }).sort().join();
  214. }
  215.  
  216. if (versions) {
  217. if (!Array.isArray(versions))
  218. versions = [versions];
  219. versions.sort();
  220. }
  221.  
  222. exists = routes.some(function (r) {
  223. return (r.name === name);
  224. });
  225. if (exists)
  226. return (false);
  227.  
  228. route = {
  229. name: name,
  230. method: options.method,
  231. path: compileURL({
  232. url: options.path || options.url,
  233. flags: options.flags,
  234. urlParamPattern: options.urlParamPattern
  235. }),
  236. spec: options,
  237. types: type,
  238. versions: versions
  239. };
  240. routes.push(route);
  241.  
  242. if (!this.reverse[route.path.source])
  243. this.reverse[route.path.source] = [];
  244.  
  245. if (this.reverse[route.path.source].indexOf(route.method) === -1)
  246. this.reverse[route.path.source].push(route.method);
  247.  
  248. this.mounts[route.name] = route;
  249.  
  250. this.emit('mount',
  251. route.method,
  252. route.path,
  253. route.types,
  254. route.versions);
  255.  
  256. return (route.name);
  257. };
  258.  
  259.  
  260. Router.prototype.unmount = function unmount(name) {
  261. var route = this.mounts[name];
  262. if (!route) {
  263. this.log.warn('router.unmount(%s): route does not exist', name);
  264. return (false);
  265. }
  266.  
  267. var reverse = this.reverse[route.path.source];
  268. var routes = this.routes[route.method];
  269. this.routes[route.method] = routes.filter(function (r) {
  270. return (r.name !== route.name);
  271. });
  272.  
  273. this.reverse[route.path.source] = reverse.filter(function (r) {
  274. return (r !== route.method);
  275. });
  276.  
  277. if (this.reverse[route.path.source].length === 0)
  278. delete this.reverse[route.path.source];
  279.  
  280. delete this.mounts[name];
  281.  
  282. return (name);
  283. };
  284.  
  285.  
  286. Router.prototype.get = function get(name, req, cb) {
  287. var params;
  288. var route = false;
  289. var routes = this.routes[req.method] || [];
  290.  
  291. for (var i = 0; i < routes.length; i++) {
  292. if (routes[i].name === name) {
  293. route = routes[i];
  294. try {
  295. params = matchURL(route.path, req);
  296. } catch (e) {
  297. }
  298. break;
  299. }
  300. }
  301.  
  302. if (route) {
  303. cb(null, route, params || {});
  304. } else {
  305. cb(new InternalError());
  306. }
  307. };
  308.  
  309.  
  310. Router.prototype.find = function find(req, res, callback) {
  311. var candidates = [];
  312. var ct = req.headers['content-type'] || DEF_CT;
  313. var cacheKey = req.method + req.url + req.version() + ct;
  314. var cacheVal;
  315. var neg;
  316. var params;
  317. var r;
  318. var reverse;
  319. var routes = this.routes[req.method] || [];
  320. var typed;
  321. var versioned;
  322.  
  323. if ((cacheVal = this.cache.get(cacheKey))) {
  324. res.methods = cacheVal.methods.slice();
  325. callback(null, cacheVal, shallowCopy(cacheVal.params));
  326. return;
  327. }
  328.  
  329. for (var i = 0; i < routes.length; i++) {
  330. try {
  331. params = matchURL(routes[i].path, req);
  332. } catch (e) {
  333. this.log.trace({err: e}, 'error parsing URL');
  334. callback(new BadRequestError(e.message));
  335. return;
  336. }
  337.  
  338. if (params === false)
  339. continue;
  340.  
  341. reverse = this.reverse[routes[i].path.source];
  342.  
  343. if (routes[i].types.length && req.isUpload()) {
  344. candidates.push({
  345. p: params,
  346. r: routes[i]
  347. });
  348. typed = true;
  349. continue;
  350. }
  351.  
  352. // GH-283: we want to find the latest version for a given route,
  353. // not the first one. However, if neither the client nor
  354. // server specified any version, we're done, because neither
  355. // cared
  356. if (routes[i].versions.length === 0 && req.version() === '*') {
  357. r = routes[i];
  358. break;
  359. }
  360.  
  361. if (routes[i].versions.length > 0) {
  362. candidates.push({
  363. p: params,
  364. r: routes[i]
  365. });
  366. versioned = true;
  367. }
  368. }
  369.  
  370. if (!r) {
  371. // If upload and typed
  372. if (typed) {
  373. /* JSSTYLED */
  374. var _t = ct.split(/\s*,\s*/);
  375. candidates = candidates.filter(function (c) {
  376. neg = new Negotiator({
  377. headers: {
  378. accept: c.r.types.join(', ')
  379. }
  380. });
  381. var tmp = neg.preferredMediaType(_t);
  382. return (tmp && tmp.length);
  383. });
  384.  
  385. // Pick the first one in case not versioned
  386. if (candidates.length) {
  387. r = candidates[0].r;
  388. params = candidates[0].p;
  389. }
  390. }
  391.  
  392. if (versioned) {
  393. var maxV;
  394. candidates.forEach(function (c) {
  395. var k = c.r.versions;
  396. var v = semver.maxSatisfying(k, req.version());
  397.  
  398. if (v) {
  399. if (!r || semver.gt(v, maxV)) {
  400. r = c.r;
  401. params = c.p;
  402. maxV = v;
  403. }
  404. }
  405. });
  406. }
  407. }
  408.  
  409. // In order, we check if the route exists, in which case, we're good.
  410. // Otherwise we look to see if ver was set to false; that would tell us
  411. // we indeed did find a matching route (method+url), but the version
  412. // field didn't line up, so we return bad version. If no route and no
  413. // version, we now need to go walk the reverse map and look at whether
  414. // we should return 405 or 404. If it was an OPTIONS request, we need
  415. // to handle this having been a preflight request.
  416. if (params && r) {
  417. cacheVal = {
  418. methods: reverse,
  419. name: r.name,
  420. params: params,
  421. spec: r.spec
  422. };
  423. this.cache.set(cacheKey, cacheVal);
  424. res.methods = reverse.slice();
  425. callback(null, cacheVal, shallowCopy(params));
  426. return;
  427. }
  428.  
  429. if (typed) {
  430. callback(new UnsupportedMediaTypeError(ct));
  431. return;
  432. }
  433. if (versioned) {
  434. callback(new InvalidVersionError('%s is not supported by %s %s',
  435. req.version() || '?',
  436. req.method,
  437. req.path()));
  438. return;
  439. }
  440.  
  441. //Checks if header is in cors.ALLOWED_HEADERS
  442. function inAllowedHeaders(header) {
  443. header = header.toLowerCase();
  444. return (cors.ALLOW_HEADERS.indexOf(header) !== -1);
  445. }
  446.  
  447. // This is a very generic preflight handler - it does
  448. // not handle requiring authentication, nor does it do
  449. // any special checking for extra user headers. The
  450. // user will need to defined their own .opts handler to
  451. // do that
  452. function preflight(methods) {
  453. var headers = req.headers['access-control-request-headers'];
  454. var method = req.headers['access-control-request-method'];
  455. var origin = req.headers['origin'];
  456.  
  457. if (req.method !== 'OPTIONS' || !origin || !method ||
  458. methods.indexOf(method) === -1) {
  459. return (false);
  460. }
  461. // Last, check request-headers
  462. /* JSSTYLED */
  463. var ok = !headers || headers.split(/\s*,\s*/).every(inAllowedHeaders);
  464.  
  465. if (!ok)
  466. return (false);
  467.  
  468. // Verify the incoming origin against the whitelist. Pass the origin
  469. // through if there is a match.
  470. if (cors.matchOrigin(req, cors.origins)) {
  471. res.setHeader('Access-Control-Allow-Origin', origin);
  472. if (cors.credentials) {
  473. res.setHeader('Access-Control-Allow-Credentials', 'true');
  474. }
  475. } else {
  476. res.setHeader('Access-Control-Allow-Origin', '*');
  477. }
  478. res.setHeader('Access-Control-Allow-Methods',
  479. methods.join(', '));
  480. res.setHeader('Access-Control-Allow-Headers',
  481. cors.ALLOW_HEADERS.join(', '));
  482. res.setHeader('Access-Control-Max-Age', 3600);
  483.  
  484. return (true);
  485. }
  486.  
  487. // Check for 405 instead of 404
  488. var urls = Object.keys(this.reverse);
  489. for (i = 0; i < urls.length; i++) {
  490. if (matchURL(new RegExp(urls[i]), req)) {
  491. res.methods = this.reverse[urls[i]].slice();
  492. res.setHeader('Allow', res.methods.join(', '));
  493. if (preflight(res.methods)) {
  494. callback(null, { name: 'preflight' });
  495. return;
  496. }
  497. var err = new MethodNotAllowedError('%s is not allowed',
  498. req.method);
  499. callback(err);
  500. return;
  501. }
  502. }
  503.  
  504. callback(new ResourceNotFoundError('%s does not exist', req.url));
  505. };
  506.  
  507.  
  508. Router.prototype.toString = function toString() {
  509. var self = this;
  510. var str = this.name + ':\n';
  511.  
  512. Object.keys(this.routes).forEach(function (k) {
  513. var routes = self.routes[k].map(function (r) {
  514. return (r.name);
  515. });
  516.  
  517. str += '\t\t' + k + ': [' + routes.join(', ') + ']\n';
  518. });
  519.  
  520. return (str);
  521. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement