UNPKG

16.7 kBJavaScriptView Raw
1/*
2Kettle Server component, corresponding directly with an express/node HTTP server
3
4Copyright 2012-2013 OCAD University
5
6Licensed under the New BSD license. You may not use this file except in
7compliance with this License.
8
9You may obtain a copy of the License at
10https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
11*/
12
13"use strict";
14
15var fluid = require("infusion"),
16 http = require("http"),
17 https = require("https"),
18 express = require("express"),
19 kettle = fluid.registerNamespace("kettle");
20
21fluid.defaults("kettle.server", {
22 gradeNames: ["fluid.component"],
23 mergePolicy: {
24 rootMiddleware: "noexpand"
25 },
26 rootMiddleware: { // free hash of middleware namespaces to components
27 urlencoded: {
28 middleware: "{middlewareHolder}.urlencoded",
29 priority: "before:json"
30 },
31 json: {
32 middleware: "{middlewareHolder}.json"
33 }
34 },
35 invokers: {
36 stop: {
37 funcName: "kettle.server.stop",
38 args: "{that}"
39 },
40 trackConnections: {
41 funcName: "kettle.server.trackConnections",
42 args: ["{that}.sockets", "{that}.httpServer"]
43 },
44 closeConnections: {
45 funcName: "kettle.server.closeConnections",
46 args: "{that}.sockets"
47 }
48 },
49 components: {
50 middlewareHolder: {
51 type: "kettle.standardMiddleware"
52 },
53 httpRouter: {
54 type: "kettle.router.http"
55 }
56 },
57 members: {
58 expressApp: "@expand:kettle.server.makeExpressApp()",
59 httpServer: "@expand:kettle.server.httpServer({that}.expressApp)",
60 dispatcher: "@expand:kettle.server.getDispatcher({that})",
61 rootSequence: "@expand:kettle.middleware.getHandlerSequence({that}, rootMiddleware, {that}.options.rootMiddleware)",
62 apps: [], // a list of kettle.app nested in this server
63 sockets: [] // a list of currently active sockets, to be aborted in case of server shutdown
64 },
65 events: {
66 onContributeMiddleware: null, // for 3rd parties to contribute root middleware using app.use
67 onContributeRouteHandlers: null, // for 3rd parties to contribute handlers using app.get/app.post etc.
68 onListen: null,
69 beforeStop: null,
70 onStopped: null
71 },
72 listeners: {
73 "onCreate.setLogging": {
74 funcName: "fluid.setLogging",
75 args: "{that}.options.logging"
76 },
77 "onCreate.contributeMiddleware": {
78 func: "{that}.events.onContributeMiddleware.fire",
79 args: "{that}",
80 priority: "after:setLogging"
81 },
82 "onCreate.contributeRouteHandlers": {
83 func: "{that}.events.onContributeRouteHandlers.fire",
84 args: "{that}",
85 priority: "after:contributeMiddleware"
86 },
87 "onCreate.registerRouteHandlers": {
88 funcName: "kettle.server.registerRouteHandlers",
89 args: "{that}",
90 priority: "after:contributeRouteHandlers"
91 },
92 "onCreate.listen": {
93 funcName: "kettle.server.listen",
94 args: "{that}",
95 priority: "after:registerRouteHandlers"
96 },
97 onListen: "{that}.trackConnections",
98 onDestroy: "{that}.stop",
99 beforeStop: "{that}.closeConnections",
100 onStopped: "kettle.server.shred({that})"
101 },
102 port: 8081,
103 logging: true
104});
105
106/** Push the supplied Kettle app onto the collection of apps managed by this server. This is called by an `onCreate` listener
107 * for the individual apps. Note that Kettle is currently not dynamic and that apps may not be destroyed separately
108 * from their host server.
109 * @param {kettle.server} server - The server with which the app is to be registered
110 * @param {kettle.app} app - The app to be registered
111 */
112kettle.server.registerApp = function (server, app) {
113 server.apps.push(app);
114};
115
116// Update and factor this table in the unlikely event we support further request types
117kettle.server.mismatchRequestMessages = {
118 "kettle.request.ws": {
119 statusCode: 400,
120 message: "Error: Mismatched request protocol - sent a WebSockets request to an endpoint expecting a plain HTTP request"
121 },
122 "kettle.request.http": {
123 statusCode: 426,
124 message: "Error: Mismatched request protocol - sent an HTTP request to an endpoint expecting a WebSockets request - upgrade required"
125 }
126};
127
128/**
129 * Options supplied to a request dispatcher by a particular variety of server
130 * @typedef dispatcherOptions
131 * @member {String} expectedRequestGrade A grade name expected to appear in the hierarchy of the request component selected
132 * to handle this request
133 */
134
135/** Checks whether the request selected for a route is compatible with the expected request type (currently, whether it
136 * matches in terms of being an HTTP or WebSockets request handler
137 * @param {String} expectedRequestGrade - The request grade expected by the server's dispatcher
138 * @param {String[]} handlerGrades - The list of grades actually present in the selected request's hierarchy
139 * @return {Object|Null} Either `null` if the request's grade hierarchy matches, or a mismatch error message structure
140 * to be dispatched to the client if there is a mismatch
141 */
142kettle.server.checkCompatibleRequest = function (expectedRequestGrade, handlerGrades) {
143 return fluid.contains(handlerGrades, expectedRequestGrade) ? null : kettle.server.mismatchRequestMessages[expectedRequestGrade];
144};
145
146/** Evaluates whether the incoming request matches the routing specification of any Kettle request attached to the server.
147 * @param {kettle.server} server - The server whose routing table should be queried
148 * @param {http.IncomingMessage} req - Node's native HTTP request object
149 * @param {dispatcherOptions} originOptions - The dispatcher's options determining the types of compatible request
150 * @return {routeMatch} A `routeMatch` structure which determines the request which will be created
151 */
152kettle.server.evaluateRoute = function (server, req, originOptions) {
153 var router = kettle.server.getRouter(server, req);
154 var match = router.match(req);
155 if (match) {
156 var handler = match.handler;
157 if (!handler.type) {
158 fluid.fail("Error in Kettle application definition - handler ", fluid.censorKeys(handler, ["app"]), " must have a request grade name registered as member \"type\"");
159 }
160 fluid.log("Invoking handler " + handler.type + " for route " + handler.route + " with expectedGrade " + originOptions.expectedRequestGrade);
161 var defaults = fluid.getMergedDefaults(handler.type, handler.gradeNames);
162 if (!fluid.hasGrade(defaults, "kettle.request")) {
163 fluid.fail("Error in Kettle application definition - couldn't load handler " + handler.type + " and gradeNames " +
164 JSON.stringify(fluid.makeArray(handler.gradeNames)) + " to a component derived from kettle.request - got defaults of " + JSON.stringify(defaults));
165 }
166 match.output.mismatchError = kettle.server.checkCompatibleRequest(originOptions.expectedRequestGrade, defaults.gradeNames);
167 if (match.output.mismatchError) { // In the case of a request type mismatch, create a special "mismatch" request handling component
168 handler.type = originOptions.expectedRequestGrade + ".mismatch";
169 handler.gradeNames = [];
170 }
171 }
172 return match;
173};
174
175/** Determine whether Kettle can finding a matching request handler, and if so, create an instance of the appropriate
176 * type
177 * @param {kettle.server} server - The server holding (via a `kettle.app`) the request definition
178 * @param {http.IncomingMessage} req - Node's native HTTP request object
179 * @param {http.ServerResponse} res - Node's native HTTP response object
180 * @param {Function} next - Express' `next` routing function
181 * @param {Object} originOptions - Options to be mixed in to any created request - generally the gradeNames specifying
182 * whether it is an HTTP or WS request
183 * @return {Boolean} `true` if a matching request was found and created
184 */
185kettle.server.checkCreateRequest = function (server, req, res, next, originOptions) {
186 var match = kettle.server.evaluateRoute(server, req, originOptions);
187 if (match) {
188 fluid.extend(req, match.output); // TODO: allow match to output to other locations
189 var handler = match.handler;
190 if (handler.prefix) {
191 /* istanbul ignore if - defensive test that we don't know how to trigger */
192 if (req.url.indexOf(handler.prefix) !== 0) {
193 fluid.fail("Failure in route matcher - request url " + req.url + " does not start with prefix " + handler.prefix + " even though it has been matched");
194 } else {
195 // This is apparently the time-honoured behaviour implemented by all express "routers" - the original url is preserved within req.originalUrl
196 req.url = req.url.substring(handler.prefix.length);
197 req.baseUrl = handler.prefix;
198 }
199 }
200 var options = fluid.extend({gradeNames: handler.gradeNames}, originOptions);
201 handler.app.requests.events.createRequest.fire({
202 type: handler.type,
203 options: options
204 }, req, res, next);
205 return true;
206 }
207 return false;
208};
209
210/** Get the router to be used for a particular request and handler in the context of a particular server. In the
211 * current implementation each server just has one hardwired router stored directly as a member, and this function
212 * returns that one.
213 * @param {kettle.server} that - The server for which the router is required
214 * @return {kettle.router} The server's router
215 */
216kettle.server.getRouter = function (that /*, req, handler */) {
217 return that.httpRouter; // default policy simply returns the single httpRouter
218};
219
220kettle.server.getDispatcher = function (that) {
221 return function (req, res, next, options) {
222 var match = kettle.server.checkCreateRequest(that, req, res, next, options);
223 if (!match) {
224 fluid.log("Kettle server getDispatcher found no matching handlers, continuing");
225 next();
226 }
227 };
228};
229
230/** Register one request handler record as it appears in an app's configuration into the server's routing table
231 * @param {kettle.server} that - The server whose router is to be updated
232 * @param {kettle.app} app - The app holding the handler record to be registered
233 * @param {handlerRecord} handler - The handler record to be registered. In this form, `method` may take the form
234 * of a comma-separated list of method specifications which this function will explode.
235 */
236kettle.server.registerOneHandler = function (that, app, handler) {
237 var router = kettle.server.getRouter(that, null, handler);
238 fluid.log("Registering request handler ", handler);
239 var extend = {
240 app: app
241 };
242 if (handler.method) {
243 var methods = fluid.transform(handler.method.split(","), fluid.trim);
244 fluid.each(methods, function (method) {
245 extend.method = method;
246 kettle.router.registerOneHandlerImpl(router, handler, extend);
247 });
248 } else {
249 kettle.router.registerOneHandlerImpl(router, handler, extend);
250 }
251};
252
253/** Register all nested request handlers held in apps nested within this server into the server's routing tables. This
254 * executes partway through the `onCreate` event for the server.
255 * @param {kettle.server} that - The Kettle server for which all nested request handlers are to be registered
256 */
257kettle.server.registerRouteHandlers = function (that) {
258 fluid.each(that.apps, function (app) {
259 fluid.each(app.options.requestHandlers, function (requestHandler, key) {
260 if (requestHandler) {
261 kettle.server.registerOneHandler(that, app, requestHandler);
262 } else {
263 // A typical style of overriding handlers sets them to `null` in derived grades - ignore these
264 // A better system will arrive with FLUID-4982 work allowing "local mergePolicies" to remove options material
265 fluid.log("Skipping empty handler with key " + key + " for app " + fluid.dumpThat(app));
266 }
267 });
268 });
269 that.expressApp.use(function (req, res, next) {
270 that.dispatcher(req, res, next, {expectedRequestGrade: "kettle.request.http"});
271 });
272};
273
274// Remove some members on stop, to give early diagnostics on erroneous late use
275kettle.server.shred = function (that) {
276 delete that.httpServer;
277 delete that.expressApp;
278};
279
280/** Stop the supplied Kettle server. The `beforeStop` event will be fired, then the server will be closed, and when
281 * that is concluded, the `onStopped` event will be fired.
282 * @param {kettle.server} that - The server to be stopped
283 */
284kettle.server.stop = function (that) {
285 if (!that.httpServer) {
286 return;
287 }
288 var port = that.options.port;
289 fluid.log("Stopping Kettle Server " + that.id + " on port ", port);
290 that.events.beforeStop.fire();
291
292 that.httpServer.close(function () {
293 fluid.log("Kettle Server " + that.id + " on port ", port, " is stopped");
294 that.events.onStopped.fire();
295 });
296};
297
298/** Forcibly call `destroy` on each sockets in the supplied array. This is done for historical reasons in which it
299 * was observed (c. 2013) that servers would not shut down in a timely way if sockets were open. Should be revisited.
300 * @param {Socket[]} sockets - The list of sockets which should be closed
301 */
302kettle.server.closeConnections = function (sockets) {
303 fluid.each(sockets, function (socket) {
304 socket.destroy();
305 });
306};
307
308/**
309 * The standard request listener signature implementing an Express app or other middleware.
310 *
311 * @callback RequestListener
312 * @param {http.IncomingMessage} req - Node's native HTTP request object
313 * @param {http.ServerResponse} res - Node's native HTTP response object
314 * @param {Function} next - Passes control to the next request listener in the processing chain/network
315 * @param {Function} err - Used to signal failure and halt processing of this section of the processing chain
316 */
317
318/**
319 * Create an HTTP server.
320 * @param {RequestListener} [app] - An optional requestListener, to be attached to the `request` event on startup
321 * @return {http.Server} A node HTTP Server
322 */
323kettle.server.httpServer = function (app) {
324 fluid.log("Initializing the HTTP server");
325 return http.createServer(app);
326};
327
328/**
329 * Create an HTTPS server.
330 * @param {Object} options - A collection of options as described in https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
331 * @param {RequestListener} [app] - An optional requestListener, to be attached to the `request` event on startup
332 * @return {http.Server} A node HTTP Server
333 */
334kettle.server.httpsServer = function (options, app) {
335 fluid.log("Initializing the HTTPS server");
336 return https.createServer(options, app);
337};
338
339/** Construct a fresh express app
340 * @return {RequestListener} A freshly initialised express app
341 */
342kettle.server.makeExpressApp = function () {
343 fluid.log("Initializing the Express app");
344 return express();
345};
346
347/** Begin the process of listening on the configured TCP/IP port for this server. Listening will be triggered,
348 * and once it has started, the `onListen` event will be fired.
349 * @param {kettle.server} that - The server for which listening should begin
350 */
351kettle.server.listen = function (that) {
352 var port = that.options.port;
353 fluid.log("Opening Kettle Server on port ", port);
354 that.httpServer.listen(port, function () {
355 fluid.log("Kettle Server " + that.id + " is listening on port " + port);
356 that.events.onListen.fire();
357 });
358};
359
360/** Keeps track of all currently open sockets so that the server can be fully shut down.
361 * @param {Socket[]} sockets - An array of sockets which will hold those that are currently open.
362 * @param {http.Server} httpServer - The node HTTP server for which open sockets should be tracked
363 */
364kettle.server.trackConnections = function (sockets, httpServer) {
365 // Keep track of all connections to be able to fully shut down the server.
366 httpServer.on("connection", function (socket) {
367 sockets.push(socket);
368 socket.on("close", function () {
369 sockets.splice(sockets.indexOf(socket), 1);
370 });
371 });
372};
373
374fluid.defaults("kettle.server.config", {
375 gradeNames: ["fluid.component"],
376 listeners: {
377 onCreate: "{that}.configure"
378 },
379 invokers: {
380 configure: "kettle.server.config.configure"
381 }
382});