1 | /*
|
2 | Routing Primitives for Kettle Servers
|
3 |
|
4 | Copyright 2015 Raising the Floor (International)
|
5 |
|
6 | Licensed under the New BSD license. You may not use this file except in
|
7 | compliance with this License.
|
8 |
|
9 | You may obtain a copy of the License at
|
10 | https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
|
11 | */
|
12 |
|
13 | /* Contains code adapted from express 4.x "layer.js":
|
14 | * Copyright(c) 2009-2013 TJ Holowaychuk
|
15 | * Copyright(c) 2013 Roman Shtylman
|
16 | * Copyright(c) 2014-2015 Douglas Christopher Wilson
|
17 | * MIT Licensed
|
18 | */
|
19 |
|
20 | ;
|
21 |
|
22 | var fluid = require("infusion"),
|
23 | urlModule = require("url"),
|
24 | kettle = fluid.registerNamespace("kettle");
|
25 |
|
26 | // Upstream dependency stolen from express 4.x
|
27 | // Note that path-to-regexp 2.0.0 breaks compatibility with our use of /* to encode middleware matches - seems unlikely we will upgrade
|
28 | kettle.pathToRegexp = require("path-to-regexp");
|
29 |
|
30 | fluid.defaults("kettle.router.http", {
|
31 | gradeNames: "fluid.component",
|
32 | members: {
|
33 | handlers: []
|
34 | },
|
35 | invokers: {
|
36 | register: {
|
37 | funcName: "kettle.router.http.register",
|
38 | args: ["{that}", "{arguments}.0"]
|
39 | },
|
40 | match: {
|
41 | funcName: "kettle.router.http.match",
|
42 | args: ["{that}.handlers", "{arguments}.0"]
|
43 | }
|
44 | }
|
45 | });
|
46 |
|
47 | /** A structure specifying a route and the request grades which will handle it.
|
48 | * See docs in "%kettle/docs/RequestHandlersAndApps.md" for more information.
|
49 | * @typedef {Object} handlerRecord
|
50 | * @member {String} type - The name of a request handling grade, which must be descended from `kettle.request`. If the
|
51 | * `method` field is filled in, the grade must be descended from `kettle.request.http`.
|
52 | * @member {String} [route] - A routing specification in the traditional format for express routes, e.g. of the form
|
53 | * "/preferences/:gpiiKey". A special form "/*" is supported indicating that the router handles all routes
|
54 | * @member {String} [method] - An HTTP method specification, possibly including multiple comma-separated values
|
55 | * @member {String} prefix - A routing prefix to be prepended to this handler's `route`. The prefix plus the route
|
56 | * expression must match the incoming request in order for this handler to be activated
|
57 | * @member {String[]} gradeNames - One or more grade names which will be mixed in to the constructed handler when it is constructed.
|
58 | */
|
59 |
|
60 | /** A "partially cooked" version of a `handlerRecord` as stored in various routing structures
|
61 | * @typedef {handlerRecord} internalHandlerRecord
|
62 | * @member {String} [method] - A single HTTP method specification
|
63 | * @member {kettle.app} app - The Kettle app for which this handler record is registered
|
64 | */
|
65 |
|
66 | /** A structure holding details of a matched route
|
67 | * @typedef routeMatch
|
68 | * @member {internalHandlerRecord} handler - The (elaborated version of the) original handler structure which led to the match
|
69 | * @member {Object} output - A free-form structure which will be merged into the resulting request. This will
|
70 | * contain at least:
|
71 | * @member {Object} output.params - A decoded hash of keys to values extracted from the incoming request by route variables
|
72 | * such as ":gpiiKey"
|
73 | */
|
74 |
|
75 | /** Registers a new route handler with this router. Note that the router is not dynamic and routes can currently not be removed.
|
76 | * Note that this is an internal method which corrupts its 2nd argument which must have been copied beforehand.
|
77 | * @param {kettle.router.http} that - The router in which the handler should be registered
|
78 | * @param {interalHandlerRecord} handler - A route handler structure
|
79 | */
|
80 | kettle.router.http.register = function (that, handler) {
|
81 | var prefix = handler.prefix || "";
|
82 | handler.regexp = kettle.pathToRegexp(prefix + handler.route, handler.keys = []);
|
83 | that.handlers.push(handler);
|
84 | };
|
85 |
|
86 | kettle.router.registerOneHandlerImpl = function (that, handler, extend) {
|
87 | var handlerCopy = fluid.extend({
|
88 | method: "get"
|
89 | }, handler, extend);
|
90 | kettle.router.http.register(that, handlerCopy);
|
91 | };
|
92 |
|
93 | /** Decodes a routing parameter which has been found to be a URL component matching the routing specification. If it is
|
94 | * a string, it will be URI decoded - if this decoding fails, an exception will be thrown.
|
95 | * @param {Any} val - The URL component to be decoded
|
96 | * @return {Any} Either the original argument if it was not a String or was an empty string, or the argument after
|
97 | * successful URI decoding.
|
98 | */
|
99 | kettle.router.http.decodeParam = function (val) {
|
100 | if (typeof val !== "string" || val.length === 0) {
|
101 | return val;
|
102 | }
|
103 | try {
|
104 | return decodeURIComponent(val);
|
105 | } catch (err) {
|
106 | err.message = "Failed to decode request routing parameter \"" + val + "\"";
|
107 | err.status = err.statusCode = 400;
|
108 | throw err;
|
109 | }
|
110 | };
|
111 |
|
112 | /** Extract the matched routing variables into a hash of names to values
|
113 | * @param {routeHandler} handler - The routeHandler which has been determined to match this request
|
114 | * @param {String[]} match - The output of regexp.exec as applied to the incoming request URL
|
115 | * @return {Object} A map of strings to strings of matched and decoded parameters
|
116 | */
|
117 | kettle.router.http.matchToParams = function (handler, match) {
|
118 | var params = {};
|
119 | for (var i = 1; i < match.length; i++) {
|
120 | var key = handler.keys[i - 1];
|
121 | var prop = key.name;
|
122 | var val = kettle.router.http.decodeParam(match[i]);
|
123 |
|
124 | if (val !== undefined) {
|
125 | params[prop] = val;
|
126 | }
|
127 | }
|
128 | return params;
|
129 | };
|
130 |
|
131 | // cf. Router.prototype.matchRequest
|
132 | /** Evaluates the URL and method of an incoming HTTP for a match in the table of route handlers.
|
133 | * @param {handlerRecord[]} handlers - An array of handlers in which a matching route is to be looked up
|
134 | * @param {http.IncomingMessage} req - Node's native HTTP request object
|
135 | * @return {routeMatch|Undefined} A matched route structure, or undefined if no route matched the incoming request
|
136 | */
|
137 | kettle.router.http.match = function (handlers, req) {
|
138 | var method = req.method.toLowerCase(),
|
139 | parsedUrl = urlModule.parse(req.url),
|
140 | path = parsedUrl.pathname;
|
141 | for (var i = 0; i < handlers.length; ++i) {
|
142 | var handler = handlers[i];
|
143 | if (method === handler.method) {
|
144 | var match = handler.regexp.exec(path);
|
145 | if (match) {
|
146 | return {
|
147 | handler: handler,
|
148 | output: {
|
149 | params: kettle.router.http.matchToParams(handler, match)
|
150 | }
|
151 | };
|
152 | }
|
153 | }
|
154 | }
|
155 | };
|