Skip to content
Open
2 changes: 2 additions & 0 deletions kettle.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ require("./lib/KettleRequest.ws.js");
require("./lib/KettleServer.js");
require("./lib/KettleServer.ws.js");
require("./lib/KettleSession.js");
require("./lib/KettleStaticMountIndexer.js");
require("./lib/KettleStaticRequestHandler.js");

kettle.loadTestingSupport = function () {
require("./lib/test/KettleTestUtils.js");
Expand Down
10 changes: 3 additions & 7 deletions lib/KettleApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,18 @@ var fluid = require("infusion");
* aggregated together into a Kettle "servers" of type <code>kettle.server</code>. A Kettle
* server corresponds directly to a node.js HTTP server (connect/express server) in terms
* of being "a thing listening on a TCP port". Configuration supplied to the
* options of all apps at their options path "handlers" is aggregated together to their enclosing
* options of all apps at their options path "handlers", as well as all nested components
* of type "kettle.staticRequestHandler" is aggregated together to their enclosing
* server.
*/

fluid.defaults("kettle.app", {
gradeNames: ["fluid.component"],
gradeNames: ["kettle.requestHolder"],
requestHandlers: {},
listeners: {
"onCreate.register": {
listener: "kettle.server.registerApp",
args: ["{kettle.server}", "{that}"]
}
},
components: {
requests: {
type: "kettle.requests"
}
}
});
15 changes: 11 additions & 4 deletions lib/KettleRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
var fluid = fluid || require("infusion"),
kettle = fluid.registerNamespace("kettle");

fluid.defaults("kettle.requests", {
/** A site where requests are created, with one dynamic component per active (HTTP) request. This may either
* be a top-level kettle.app or a kettle.staticRequestHandler.
*/

fluid.defaults("kettle.requestHolder", {
gradeNames: ["fluid.component"],
events: {
createRequest: null // fired by our handler once express determines that route + verb is a match
Expand Down Expand Up @@ -185,7 +189,7 @@ fluid.defaults("kettle.request", {
},
"onCreate.handle": {
funcName: "kettle.request.initiateHandleRequest",
args: ["{that}", "{kettle.server}.rootSequence"],
args: ["{that}", "{kettle.requestHolder}", "{kettle.server}.rootSequence"],
priority: "last"
},
"onHandle.handleRequest": {
Expand Down Expand Up @@ -323,13 +327,16 @@ kettle.request.sequenceRequest = function (fullSequence, request) {
* `handleRequest" method, and pass control to the request's `handleFullRequest` method which sequences them.
* @param {kettle.request} request - The request whose handling is to be initiated. This is triggered as the last action
* of the request's `onCreate' event.
* @param {kettle.requestHolder} requestHolder - A `kettle.requestHolder' in which this request is contained - most likely
* a {kettle.staticRequestHandler} which may hold some further middleware
* @param {requestTask[]} rootSequence - Array of tasks representing the parent server's root middleware sequence which
* precedes any bound to this request
*/
kettle.request.initiateHandleRequest = function (request, rootSequence) {
kettle.request.initiateHandleRequest = function (request, requestHolder, rootSequence) {
fluid.log("Kettle server allocated request object with type ", request.typeName);
var requestSequence = kettle.middleware.getHandlerSequence(request, "requestMiddleware");
var fullSequence = rootSequence.concat(requestSequence).concat([kettle.request.handleRequestTask]);
var holderSequence = kettle.middleware.getHandlerSequence(requestHolder, "requestMiddleware");
var fullSequence = rootSequence.concat(holderSequence).concat(requestSequence).concat([kettle.request.handleRequestTask]);
var handleRequestPromise = kettle.request.sequenceRequest(fullSequence, request);
request.handleFullRequest(request, handleRequestPromise, request.next);
handleRequestPromise.then(kettle.request.clear, kettle.request.clear);
Expand Down
21 changes: 9 additions & 12 deletions lib/KettleRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ kettle.pathToRegexp = require("path-to-regexp");
fluid.defaults("kettle.router.http", {
gradeNames: "fluid.component",
members: {
handlers: []
rawHandlers: [],
sortedHandlers: []
},
invokers: {
register: {
Expand All @@ -39,7 +40,7 @@ fluid.defaults("kettle.router.http", {
},
match: {
funcName: "kettle.router.http.match",
args: ["{that}.handlers", "{arguments}.0"]
args: ["{that}.sortedHandlers", "{arguments}.0"]
}
}
});
Expand All @@ -60,7 +61,7 @@ fluid.defaults("kettle.router.http", {
/** A "partially cooked" version of a `handlerRecord` as stored in various routing structures
* @typedef {handlerRecord} internalHandlerRecord
* @member {String} [method] - A single HTTP method specification
* @member {kettle.app} app - The Kettle app for which this handler record is registered
* @member {kettle.requestHolder} requestHolder - The Kettle requestHolder for which this handler record is registered, will have its createRequest event fired
*/

/** A structure holding details of a matched route
Expand All @@ -75,19 +76,15 @@ fluid.defaults("kettle.router.http", {
/** Registers a new route handler with this router. Note that the router is not dynamic and routes can currently not be removed.
* Note that this is an internal method which corrupts its 2nd argument which must have been copied beforehand.
* @param {kettle.router.http} that - The router in which the handler should be registered
* @param {interalHandlerRecord} handler - A route handler structure
* @param {internalHandlerRecord} handler - A route handler structure
*/
kettle.router.http.register = function (that, handler) {
// TODO: "prefix" is supported for all handlers but untested outside StaticRequestHandlerTests. Need to make clear
// semantics for trailing slash (which should not be stored, probably).
var prefix = handler.prefix || "";
handler.regexp = kettle.pathToRegexp(prefix + handler.route, handler.keys = []);
that.handlers.push(handler);
};

kettle.router.registerOneHandlerImpl = function (that, handler, extend) {
var handlerCopy = fluid.extend({
method: "get"
}, handler, extend);
kettle.router.http.register(that, handlerCopy);
that.rawHandlers.push(handler);
that.sortedHandlers = fluid.parsePriorityRecords(that.rawHandlers, "Kettle route handlers");
};

/** Decodes a routing parameter which has been found to be a URL component matching the routing specification. If it is
Expand Down
55 changes: 41 additions & 14 deletions lib/KettleServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ fluid.defaults("kettle.server", {
},
httpRouter: {
type: "kettle.router.http"
},
staticMountIndexer: {
type: "kettle.staticMountIndexer"
}
},
members: {
Expand All @@ -64,6 +67,7 @@ fluid.defaults("kettle.server", {
sockets: [] // a list of currently active sockets, to be aborted in case of server shutdown
},
events: {
onRegisterHandler: null, // fired outwards when a requestHandler is registered
onContributeMiddleware: null, // for 3rd parties to contribute root middleware using app.use
onContributeRouteHandlers: null, // for 3rd parties to contribute handlers using app.get/app.post etc.
onListen: null,
Expand Down Expand Up @@ -98,6 +102,9 @@ fluid.defaults("kettle.server", {
args: "{that}",
priority: "after:registerRouteHandlers"
},
"onRegisterHandler.index": {
func: "{staticMountIndexer}.indexMount"
},
"onCreate.trackConnections": {
func: "{that}.trackConnections"
},
Expand Down Expand Up @@ -188,8 +195,8 @@ kettle.server.evaluateRoute = function (server, req, originOptions) {
* @param {http.IncomingMessage} req - Node's native HTTP request object
* @param {http.ServerResponse} res - Node's native HTTP response object
* @param {Function} next - Express' `next` routing function
* @param {Object} originOptions - Options to be mixed in to any created request - generally the gradeNames specifying
* whether it is an HTTP or WS request
* @param {Object} originOptions - Options to be mixed in to any created request - generally the `expectedRequestGrade`
* member specifying whether it is expected to resolve to an HTTP or WS request
* @return {Boolean} `true` if a matching request was found and created
*/
kettle.server.checkCreateRequest = function (server, req, res, next, originOptions) {
Expand All @@ -198,6 +205,7 @@ kettle.server.checkCreateRequest = function (server, req, res, next, originOptio
fluid.extend(req, match.output); // TODO: allow match to output to other locations
var handler = match.handler;
if (handler.prefix) {
// Note that with a prefix of empty string we rely on edge cases in String.indexOf to match at position 0
/* istanbul ignore if - defensive test that we don't know how to trigger */
if (req.url.indexOf(handler.prefix) !== 0) {
fluid.fail("Failure in route matcher - request url " + req.url + " does not start with prefix " + handler.prefix + " even though it has been matched");
Expand All @@ -207,8 +215,8 @@ kettle.server.checkCreateRequest = function (server, req, res, next, originOptio
req.baseUrl = handler.prefix;
}
}
var options = fluid.extend({gradeNames: handler.gradeNames}, originOptions);
handler.app.requests.events.createRequest.fire({
var options = fluid.extend({gradeNames: handler.gradeNames}, handler.options, originOptions);
handler.requestHolder.events.createRequest.fire({
type: handler.type,
options: options
}, req, res, next);
Expand All @@ -224,7 +232,7 @@ kettle.server.checkCreateRequest = function (server, req, res, next, originOptio
* @return {kettle.router} The server's router
*/
kettle.server.getRouter = function (that /*, req, handler */) {
return that.httpRouter; // default policy simply returns the single httpRouter
return fluid.getForComponent(that, "httpRouter"); // default policy simply returns the single httpRouter
};

kettle.server.getDispatcher = function (that) {
Expand All @@ -237,26 +245,45 @@ kettle.server.getDispatcher = function (that) {
};
};

/** Registers a new route handler in the supplied router.
* @param {kettle.server} that - The `kettle.server` to which the router is attached - `onRegisterHandler` event will be fired
* @param {kettle.router.http} router - The router in which the handler should be registered
* @param {internalHandlerRecord[]} handlerStack - A stack of handlerRecord structures which should be merged together
* in the supplied order to arrive at the record to be registered.
*/
kettle.server.registerOneHandlerImpl = function (that, router, handlerStack) {
var mergedHandler = fluid.extend.apply(null, [{}].concat(handlerStack));
kettle.router.http.register(router, mergedHandler);
that.events.onRegisterHandler.fire(mergedHandler);
};

/** Register one request handler record as it appears in an app's configuration into the server's routing table
* @param {kettle.server} that - The server whose router is to be updated
* @param {kettle.app} app - The app holding the handler record to be registered
* @param {kettle.requestHolder} requestHolder - The requestHolder holding the handler record to be registered, will have its createRequest event fired
* @param {handlerRecord} handler - The handler record to be registered. In this form, `method` may take the form
* of a comma-separated list of method specifications which this function will explode.
* @param {String} key - The key of this handler record within its containing structure - will be used as a default value for
* the `namespace` element of the handlerRecord if none is listed
*/
kettle.server.registerOneHandler = function (that, app, handler) {
kettle.server.registerOneHandler = function (that, requestHolder, handler, key) {
var router = kettle.server.getRouter(that, null, handler);
fluid.log("Registering request handler ", handler);
var extend = {
app: app
fluid.log("Registering request handler ", handler, " with key " + key);
var lowOptions = {
method: "get",
namespace: key
};
var highOptions = {
requestHolder: requestHolder
};
var handlerStack = [lowOptions, handler, highOptions];
if (handler.method) {
var methods = fluid.transform(handler.method.split(","), fluid.trim);
fluid.each(methods, function (method) {
extend.method = method;
kettle.router.registerOneHandlerImpl(router, handler, extend);
highOptions.method = method;
kettle.server.registerOneHandlerImpl(that, router, handlerStack);
});
} else {
kettle.router.registerOneHandlerImpl(router, handler, extend);
kettle.server.registerOneHandlerImpl(that, router, handlerStack);
}
};

Expand All @@ -268,7 +295,7 @@ kettle.server.registerRouteHandlers = function (that) {
fluid.each(that.apps, function (app) {
fluid.each(app.options.requestHandlers, function (requestHandler, key) {
if (requestHandler) {
kettle.server.registerOneHandler(that, app, requestHandler);
kettle.server.registerOneHandler(that, app, requestHandler, key);
} else {
// A typical style of overriding handlers sets them to `null` in derived grades - ignore these
// A better system will arrive with FLUID-4982 work allowing "local mergePolicies" to remove options material
Expand Down
93 changes: 93 additions & 0 deletions lib/KettleStaticMountIndexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Kettle Static Mount Indexer

Copyright 2019 Raising the Floor - International

Licensed under the New BSD license. You may not use this file except in
compliance with this License.

You may obtain a copy of the License at
https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
*/

"use strict";

// Adapted from fluid-authoring "Visible Nexus" instance of IncludeRewriting.js
// makes use of staticRequestHandler info only to avoid blind introspection into request-scope components
var fluid = require("infusion"),
kettle = fluid.registerNamespace("kettle");

fluid.defaults("kettle.staticMountIndexer", {
gradeNames: "fluid.component",
members: {
mountTable: []
},
invokers: {
indexMount: {
funcName: "kettle.staticMountIndexer.indexMount",
args: ["{that}", "{arguments}.0"]
}
}
});

kettle.staticMountIndexer.splitModulePath = function (modulePath) {
if (modulePath.charAt(0) !== "%") {
return null;
}
var slashPos = modulePath.indexOf("/");
if (slashPos === -1) {
slashPos = modulePath.length;
}
return {
moduleName: modulePath.substring(1, slashPos),
suffix: modulePath.substring(slashPos + 1, modulePath.length)
};
};

/** Index any static middleware attached to the requestHolder, imputed to be a staticRequestHandler, referenced in
* the supplied requestHandler which has presumably just been registered into the server's routing table.
* @param {kettle.staticMountIndexer} that - The staticMountIndexer in which the index is to be registered
* @param {internalHandlerRecord} requestHandler - The record which has just been registered
*/
kettle.staticMountIndexer.indexMount = function (that, requestHandler) {
var staticRequestHandler = requestHandler.requestHolder;
fluid.each(staticRequestHandler.options.requestMiddleware, function (oneMiddleware) {
var middleware = oneMiddleware.middleware;
if (fluid.componentHasGrade(middleware, "kettle.middleware.static")) {
var root = middleware.options.root;
var parsedRoot = kettle.staticMountIndexer.splitModulePath(root);
var prefix = requestHandler.prefix === "/" ? "" : requestHandler.prefix;
that.mountTable.push({
moduleName: parsedRoot.moduleName,
suffix: parsedRoot.suffix,
prefix: prefix
});
};
});
};


fluid.registerNamespace("kettle.includeRewriter");

/** Rewrites a module-relative URL of the form %module-name/some/suffix so that it takes the form of an actual
* URL hosted by some static middleware hosting that module's content in a server's URL space.
* @param {kettle.staticMountIndexer} staticMountIndexer - The `kettle.staticMountIndexer` component holding the mount table to be inspected
* @param {String} url - A URL to be rewritten, perhaps beginning with a %-qualified module prefix
* @return {String|Null} If the supplied URL was module-qualified, and it could be resolved, the resolved value is
* returned, or else null if it could not. If the supplied URL was not module-qualified, it is returned unchanged.
*/
kettle.staticMountIndexer.rewriteUrl = function (staticMountIndexer, url) {
var mountTable = staticMountIndexer.mountTable;
var parsed = kettle.staticMountIndexer.splitModulePath(url);
if (parsed) {
for (var i = 0; i < mountTable.length; ++i) {
var mount = mountTable[i];
if (mount.moduleName === parsed.moduleName && parsed.suffix.startsWith(mount.suffix)) {
var endSuffix = parsed.suffix.substring(mount.suffix.length);
return mount.prefix + (endSuffix.startsWith("/") ? "" : "/") + endSuffix;
}
}
return null;
}
return url;
};
Loading