UNPKG

12.2 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}, {that}.rootSequence)",
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.registerDispatchHandler": {
88 funcName: "kettle.server.registerDispatchHandler",
89 args: "{that}",
90 priority: "after:contributeRouteHandlers"
91 },
92 "onCreate.listen": {
93 funcName: "kettle.server.listen",
94 args: "{that}",
95 priority: "after:registerHandler"
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
106kettle.server.registerApp = function (server, app) {
107 server.apps.push(app);
108};
109
110// Update and factor this table in the unlikely event we support further request types
111kettle.server.mismatchRequestMessages = {
112 "kettle.request.ws": {
113 statusCode: 400,
114 message: "Error: Mismatched request protocol - sent a WebSockets request to an endpoint expecting a plain HTTP request"
115 },
116 "kettle.request.http": {
117 statusCode: 426,
118 message: "Error: Mismatched request protocol - sent an HTTP request to an endpoint expecting a WebSockets request - upgrade required"
119 }
120};
121
122kettle.server.checkCompatibleRequest = function (expectedRequestGrade, handlerGrades) {
123 return fluid.contains(handlerGrades, expectedRequestGrade) ? null : kettle.server.mismatchRequestMessages[expectedRequestGrade];
124};
125
126kettle.server.evaluateRoute = function (server, req, originOptions) {
127 var router = kettle.server.getRouter(server, req);
128 var match = router.match(req);
129 if (match) {
130 var handler = match.handler;
131 if (!handler.type) {
132 fluid.fail("Error in Kettle application definition - handler ", fluid.censorKeys(handler, ["app"]), " must have a request grade name registered as member \"type\"");
133 }
134 fluid.log("Invoking handler " + handler.type + " for route " + handler.route + " with expectedGrade " + originOptions.expectedRequestGrade);
135 var defaults = fluid.getMergedDefaults(handler.type, handler.gradeNames);
136 if (!fluid.hasGrade(defaults, "kettle.request")) {
137 fluid.fail("Error in Kettle application definition - couldn't load handler " + handler.type + " and gradeNames " +
138 JSON.stringify(fluid.makeArray(handler.gradeNames)) + " to a component derived from kettle.request - got defaults of " + JSON.stringify(defaults));
139 }
140 match.output.mismatchError = kettle.server.checkCompatibleRequest(originOptions.expectedRequestGrade, defaults.gradeNames);
141 if (match.output.mismatchError) { // In the case of a request type mismatch, create a special "mismatch" request handling component
142 handler.type = originOptions.expectedRequestGrade + ".mismatch";
143 handler.gradeNames = [];
144 }
145 }
146 return match;
147};
148
149kettle.server.checkCreateRequest = function (server, req, res, next, originOptions) {
150 var match = kettle.server.evaluateRoute(server, req, originOptions);
151 if (match) {
152 fluid.extend(req, match.output); // TODO: allow match to output to other locations
153 var handler = match.handler;
154 if (handler.prefix) {
155 /* istanbul ignore if - defensive test that we don't know how to trigger */
156 if (req.url.indexOf(handler.prefix) !== 0) {
157 fluid.fail("Failure in route matcher - request url " + req.url + " does not start with prefix " + handler.prefix + " even though it has been matched");
158 } else {
159 // This is apparently the time-honoured behaviour implemented by all express "routers" - the original url is preserved within req.originalUrl
160 req.url = req.url.substring(handler.prefix.length);
161 req.baseUrl = handler.prefix;
162 }
163 }
164 var options = fluid.extend({gradeNames: handler.gradeNames}, originOptions);
165 handler.app.requests.events.createRequest.fire({
166 type: handler.type,
167 options: options
168 }, req, res, next);
169 }
170 return req.fluidRequest; // we've either created one or we haven't
171};
172
173kettle.server.getRouter = function (that /*, req, handler */) {
174 return that.httpRouter; // default policy simply returns the single httpRouter
175};
176
177kettle.server.sequenceRequest = function (fullSequence, request) {
178 var sequence = fluid.promise.sequence(fullSequence, request);
179 var togo = fluid.promise();
180 sequence.then(function () { // only the handler's promise return counts for success resolution
181 fluid.promise.follow(request.handlerPromise, togo);
182 }, togo.reject);
183 return togo;
184};
185
186kettle.server.getDispatcher = function (that, rootSequence) {
187 return function (req, res, next, options) {
188 var request = kettle.server.checkCreateRequest(that, req, res, next, options);
189 if (request) {
190 fluid.log("Kettle server allocated request object with type ", request.typeName);
191 var requestSequence = kettle.middleware.getHandlerSequence(request, "requestMiddleware");
192 var fullSequence = rootSequence.concat(requestSequence).concat([kettle.request.handleRequestTask]);
193 var handleRequestPromise = kettle.server.sequenceRequest(fullSequence, request);
194 request.handleFullRequest(request, handleRequestPromise, next);
195 handleRequestPromise.then(kettle.request.clear, kettle.request.clear);
196 } else {
197 fluid.log("Kettle server getDispatcher found no matching handlers, continuing");
198 next();
199 }
200 };
201};
202
203kettle.server.registerOneHandler = function (that, app, handler) {
204 var router = kettle.server.getRouter(that, null, handler);
205 fluid.log("Registering request handler ", handler);
206 var extend = {
207 app: app
208 };
209 if (handler.method) {
210 var methods = fluid.transform(handler.method.split(","), fluid.trim);
211 fluid.each(methods, function (method) {
212 extend.method = method;
213 kettle.router.registerOneHandlerImpl(router, handler, extend);
214 });
215 } else {
216 kettle.router.registerOneHandlerImpl(router, handler, extend);
217 }
218};
219
220kettle.server.registerDispatchHandler = function (that) {
221 fluid.each(that.apps, function (app) {
222 fluid.each(app.options.requestHandlers, function (requestHandler, key) {
223 if (requestHandler) {
224 kettle.server.registerOneHandler(that, app, requestHandler);
225 } else {
226 // A typical style of overriding handlers sets them to `null` in derived grades - ignore these
227 // A better system will arrive with FLUID-4982 work allowing "local mergePolicies" to remove options material
228 fluid.log("Skipping empty handler with key " + key + " for app " + fluid.dumpThat(app));
229 }
230 });
231 });
232 that.expressApp.use(function (req, res, next) {
233 that.dispatcher(req, res, next, {expectedRequestGrade: "kettle.request.http"});
234 });
235};
236
237// Remove some members on stop, to give early diagnostics on erroneous late use
238kettle.server.shred = function (that) {
239 delete that.httpServer;
240 delete that.expressApp;
241};
242
243kettle.server.stop = function (that) {
244 if (!that.httpServer) {
245 return;
246 }
247 var port = that.options.port;
248 fluid.log("Stopping Kettle Server " + that.id + " on port ", port);
249 that.events.beforeStop.fire();
250
251 that.httpServer.close(function () {
252 fluid.log("Kettle Server " + that.id + " on port ", port, " is stopped");
253 that.events.onStopped.fire();
254 });
255};
256
257kettle.server.closeConnections = function (sockets) {
258 // The server will not actually be closed unless all connections are
259 // closed. Force close all connections.
260 fluid.each(sockets, function (socket) {
261 socket.destroy();
262 });
263};
264
265/**
266 * Create an HTTP server.
267 * @param {Object} server A Request Listener, if any.
268 * @return {Object} A node HTTP Server
269 */
270kettle.server.httpServer = function (server) {
271 fluid.log("Initializing the HTTP server");
272 return http.createServer(server);
273};
274
275kettle.server.httpsServer = function (options, app) {
276 fluid.log("Initializing the HTTPS server");
277 return https.createServer(options, app);
278};
279
280kettle.server.makeExpressApp = function () {
281 fluid.log("Initializing the Express app");
282 return express();
283};
284
285kettle.server.listen = function (that) {
286 var port = that.options.port;
287 fluid.log("Opening Kettle Server on port ", port);
288 that.httpServer.listen(port, function () {
289 fluid.log("Kettle Server " + that.id + " is listening on port " + port);
290 that.events.onListen.fire();
291 });
292};
293
294kettle.server.trackConnections = function (sockets, httpServer) {
295 // Keep track of all connections to be able to fully shut down the server.
296 httpServer.on("connection", function (socket) {
297 sockets.push(socket);
298 socket.on("close", function () {
299 sockets.splice(sockets.indexOf(socket), 1);
300 });
301 });
302};
303
304fluid.defaults("kettle.server.config", {
305 gradeNames: ["fluid.component"],
306 listeners: {
307 onCreate: "{that}.configure"
308 },
309 invokers: {
310 configure: "kettle.server.config.configure"
311 }
312});