Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Server: Add handling for custom middleware #200

Merged
merged 9 commits into from
Jul 10, 2019
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
## Server
Provides server capabilities for the [UI5 Tooling](https://github.com/SAP/ui5-tooling).

### Middlewares
The development server has already a set of middlewares which supports the developer with the following features:
### Middleware
The development server has already a set of middleware which supports the developer with the following features:

* Translation files with `.properties` extension are properly encoded with **ISO-8859-1**.
* Changes on files with `.less` extension triggers a theme build and delivers the compiled CSS files.
Expand Down
32 changes: 26 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,33 @@
module.exports = {
server: require("./lib/server"),
sslUtil: require("./lib/sslUtil"),
middlewareRepository: require("./lib/middleware/middlewareRepository"),

// Legacy middleware export. Still private.
middleware: {
csp: require("./lib/middleware/csp"),
discovery: require("./lib/middleware/discovery"),
nonReadRequests: require("./lib/middleware/discovery"),
serveIndex: require("./lib/middleware/serveIndex"),
serveResources: require("./lib/middleware/serveResources"),
serveThemes: require("./lib/middleware/serveThemes"),
versionInfo: require("./lib/middleware/versionInfo"),
discovery: mapLegacyMiddlewareArguments(require("./lib/middleware/discovery")),
nonReadRequests: mapLegacyMiddlewareArguments(require("./lib/middleware/discovery")),
serveIndex: mapLegacyMiddlewareArguments(require("./lib/middleware/serveIndex")),
serveResources: mapLegacyMiddlewareArguments(require("./lib/middleware/serveResources")),
serveThemes: mapLegacyMiddlewareArguments(require("./lib/middleware/serveThemes")),
versionInfo: mapLegacyMiddlewareArguments(require("./lib/middleware/versionInfo")),
}
};

function mapLegacyMiddlewareArguments(module) {
// Old arguments was a single object with optional properties
// - resourceCollections
// - tree
return function({resourceCollections, tree} = {}) {
const resources = {};
resources.all = resourceCollections.combo;
resources.rootProject = resourceCollections.source;
resources.dependencies = resourceCollections.dependencies;

return module({
resources,
tree
});
};
}
178 changes: 178 additions & 0 deletions lib/middleware/MiddlewareManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
const middlewareRepository = require("./middlewareRepository");
/**
*
*
* @memberof module:@ui5/server.middleware
*/
class MiddlewareManager {
constructor({tree, resources, options = {
sendSAPTargetCSP: false
}}) {
if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) {
throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided");
}
this.tree = tree;
this.resources = resources;
this.options = options;

this.middleware = {};
this.middlewareExecutionOrder = [];
}

async applyMiddleware(app) {
await this.addStandardMiddleware();
await this.addCustomMiddleware();

return this.middlewareExecutionOrder.map((name) => {
const m = this.middleware[name];
app.use(m.mountPath, m.middleware);
});
}

async addMiddleware(middlewareName, {
wrapperCallback, mountPath = "/",
beforeMiddleware, afterMiddleware
} = {}) {
let middlewareCallback = middlewareRepository.getMiddleware(middlewareName);
if (wrapperCallback) {
middlewareCallback = wrapperCallback(middlewareCallback);
}
if (this.middleware[middlewareName] || this.middlewareExecutionOrder.includes(middlewareName)) {
throw new Error(`Failed to add duplicate middleware ${middlewareName}`);
}

if (beforeMiddleware || afterMiddleware) {
const refMiddlewareName = beforeMiddleware || afterMiddleware;
let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName);
if (refMiddlewareIdx === -1) {
throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` +
`middleware ${middlewareName}`);
}
if (afterMiddleware) {
// Insert after index of referenced middleware
refMiddlewareIdx++;
}
this.middlewareExecutionOrder.splice(refMiddlewareIdx, 0, middlewareName);
} else {
this.middlewareExecutionOrder.push(middlewareName);
}

this.middleware[middlewareName] = {
middleware: await Promise.resolve(middlewareCallback({resources: this.resources})),
mountPath
};
}

async addStandardMiddleware() {
await this.addMiddleware("csp", {
wrapperCallback: (cspModule) => {
const oCspConfig = {
allowDynamicPolicySelection: true,
allowDynamicPolicyDefinition: true,
definedPolicies: {
"sap-target-level-1":
"default-src 'self'; " +
"script-src 'self' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"font-src 'self' data:; " +
"img-src 'self' * data: blob:; " +
"frame-src 'self' https: data: blob:; " +
"child-src 'self' https: data: blob:; " +
"connect-src 'self' https: wss:;",
"sap-target-level-2":
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"font-src 'self' data:; " +
"img-src 'self' * data: blob:; " +
"frame-src 'self' https: data: blob:; " +
"child-src 'self' https: data: blob:; " +
"connect-src 'self' https: wss:;"
}
};
if (this.options.sendSAPTargetCSP) {
Object.assign(oCspConfig, {
defaultPolicy: "sap-target-level-1",
defaultPolicyIsReportOnly: true,
defaultPolicy2: "sap-target-level-2",
defaultPolicy2IsReportOnly: true,
});
}
return () => {
return cspModule("sap-ui-xx-csp-policy", oCspConfig);
};
}
});
await this.addMiddleware("compression");
await this.addMiddleware("cors");
await this.addMiddleware("discovery", {
mountPath: "/discovery"
});
await this.addMiddleware("serveResources");
await this.addMiddleware("serveThemes");
await this.addMiddleware("versionInfo", {
mountPath: "/resources/sap-ui-version.json",
wrapperCallback: (versionInfoModule) => {
return ({resources}) => {
return versionInfoModule({
resources,
tree: this.tree
});
};
}
});
await this.addMiddleware("connectUi5Proxy", {
mountPath: "/proxy"
});
// Handle anything but read operations *before* the serveIndex middleware
// as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling
await this.addMiddleware("nonReadRequests");
await this.addMiddleware("serveIndex");
}

async addCustomMiddleware() {
const project = this.tree;
const projectCustomMiddleware = project.server && project.server.customMiddleware;
if (!projectCustomMiddleware || projectCustomMiddleware.length === 0) {
return; // No custom middleware defined
}

for (let i = 0; i < projectCustomMiddleware.length; i++) {
const middlewareDef = projectCustomMiddleware[i];
if (!middlewareDef.name) {
throw new Error(`Missing name for custom middleware definition of project ${project.metadata.name} ` +
`at index ${i}`);
}
if (middlewareDef.beforeMiddleware && middlewareDef.afterMiddleware) {
throw new Error(
`Custom middleware definition ${middlewareDef.name} of project ${project.metadata.name} ` +
`defines both "beforeMiddleware" and "afterMiddleware" parameters. Only one must be defined.`);
}
if (!middlewareDef.beforeMiddleware && !middlewareDef.afterMiddleware) {
throw new Error(
`Custom middleware definition ${middlewareDef.name} of project ${project.metadata.name} ` +
`defines neither a "beforeMiddleware" nor an "afterMiddleware" parameter. One must be defined.`);
}

if (this.middleware[middlewareDef.name]) {
// Middleware is already known
throw new Error(`Failed to add custom middleware ${middlewareDef.name}. ` +
`A middleware with the same name is already known.`);
}
await this.addMiddleware(middlewareDef.name, {
wrapperCallback: (middleware) => {
return ({resources}) => {
const options = {
configuration: middlewareDef.configuration
};
return middleware({resources, options});
};
},
mountPath: middlewareDef.mountPath,
beforeMiddleware: middlewareDef.beforeMiddleware,
afterMiddleware: middlewareDef.afterMiddleware
});
}
}
}
module.exports = MiddlewareManager;
9 changes: 9 additions & 0 deletions lib/middleware/connectUi5Proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const ui5connect = require("connect-openui5");

function createMiddleware() {
return ui5connect.proxy({
secure: false
});
}

module.exports = createMiddleware;
18 changes: 10 additions & 8 deletions lib/middleware/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const urlPattern = /\/(app_pages|all_libs|all_tests)(?:[?#].*)?$/;
* </ul>
*
* @module @ui5/server/middleware/discovery
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resourceCollections.source Resource reader or collection for the source project
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
* @param {Object} parameters Parameters
* @param {module:@ui5/fs.AbstractReader} parameters.resources.all Reader or Collection to read resources of the
* root project and its dependencies
* @param {module:@ui5/fs.AbstractReader} parameters.resources.rootProject Reader or Collection to read resources of
* the project the server is started in
* @returns {Function} Returns a server middleware closure.
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
*/
function createMiddleware({resourceCollections}) {
function createMiddleware({resources}) {
return function discoveryMiddleware(req, res, next) {
const parts = urlPattern.exec(req.url);
const type = parts && parts[1];
Expand Down Expand Up @@ -46,7 +48,7 @@ function createMiddleware({resourceCollections}) {
}

if (type === "app_pages") {
resourceCollections.source.byGlob("/**/*.{html,htm}").then(function(resources) {
resources.rootProject.byGlob("/**/*.{html,htm}").then(function(resources) {
resources.forEach(function(resource) {
const relPath = resource.getPath().substr(1); // cut off leading "/"
response.push({
Expand All @@ -56,7 +58,7 @@ function createMiddleware({resourceCollections}) {
sendResponse();
});
} else if (type === "all_libs") {
resourceCollections.combo.byGlob([
resources.all.byGlob([
"/resources/**/*.library"
]).then(function(resources) {
resources.forEach(function(resource) {
Expand All @@ -72,8 +74,8 @@ function createMiddleware({resourceCollections}) {
});
} else if (type === "all_tests") {
Promise.all([
resourceCollections.combo.byGlob("/resources/**/*.library"),
resourceCollections.combo.byGlob("/test-resources/**/*.{html,htm}")
resources.all.byGlob("/resources/**/*.library"),
resources.all.byGlob("/test-resources/**/*.{html,htm}")
]).then(function(results) {
const libraryResources = results[0];
const testPageResources = results[1];
Expand Down
33 changes: 33 additions & 0 deletions lib/middleware/middlewareRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const middlewares = {
compression: "compression",
cors: "cors",
csp: "./csp",
serveResources: "./serveResources",
serveIndex: "./serveIndex",
discovery: "./discovery",
versionInfo: "./versionInfo",
connectUi5Proxy: "./connectUi5Proxy",
serveThemes: "./serveThemes",
nonReadRequests: "./nonReadRequests"
};

function getMiddleware(middlewareName) {
const middlewarePath = middlewares[middlewareName];

if (!middlewarePath) {
throw new Error(`middlewareRepository: Unknown Middleware ${middlewareName}`);
}
return require(middlewarePath);
}

function addMiddleware(name, middlewarePath) {
if (middlewares[name]) {
throw new Error(`middlewareRepository: Middleware ${name} already registered`);
}
middlewares[name] = middlewarePath;
}

module.exports = {
getMiddleware: getMiddleware,
addMiddleware: addMiddleware
};
8 changes: 4 additions & 4 deletions lib/middleware/serveIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,16 @@ function createContent(path, resourceInfos) {
* Creates and returns the middleware to serve a resource index.
*
* @module @ui5/server/middleware/serveIndex
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
* @param {Object} resources Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
* @returns {Function} Returns a server middleware closure.
*/
function createMiddleware({resourceCollections}) {
function createMiddleware({resources}) {
return function serveIndex(req, res, next) {
const pathname = parseurl(req).pathname;
log.verbose("\n Listing index of " + pathname);
const glob = pathname + (pathname.endsWith("/") ? "*" : "/*");
resourceCollections.combo.byGlob(glob, {nodir: false}).then((resources) => {
resources.all.byGlob(glob, {nodir: false}).then((resources) => {
if (!resources || resources.length == 0) { // Not found
next();
return;
Expand Down
8 changes: 4 additions & 4 deletions lib/middleware/serveResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ function isFresh(req, res) {
* Creates and returns the middleware to serve application resources.
*
* @module @ui5/server/middleware/serveResources
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
* @param {Object} resources Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
* @returns {Function} Returns a server middleware closure.
*/
function createMiddleware({resourceCollections}) {
function createMiddleware({resources}) {
return function serveResources(req, res, next) {
const pathname = parseurl(req).pathname;
resourceCollections.combo.byPath(pathname).then(function(resource) {
resources.all.byPath(pathname).then(function(resource) {
if (!resource) { // Not found
next();
return;
Expand Down
10 changes: 5 additions & 5 deletions lib/middleware/serveThemes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ const themeRequest = /^(.*\/)library(?:(\.css)|(-RTL\.css)|(-parameters\.json))$
* The theme is built in realtime. If a less file was modified, the theme build is triggered to rebuild the theme.
*
* @module @ui5/server/middleware/serveThemes
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
* @param {Object} resources Contains the resource reader or collection to access project related files
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
* @returns {Function} Returns a server middleware closure.
*/
function createMiddleware({resourceCollections}) {
function createMiddleware({resources}) {
const builder = new themeBuilder.ThemeBuilder({
fs: fsInterface(resourceCollections.combo)
fs: fsInterface(resources.all)
});

return function theme(req, res, next) {
Expand All @@ -46,7 +46,7 @@ function createMiddleware({resourceCollections}) {
}

const sourceLessPath = themeReq[1] + "library.source.less";
resourceCollections.combo.byPath(sourceLessPath).then((sourceLessResource) => {
resources.all.byPath(sourceLessPath).then((sourceLessResource) => {
if (!sourceLessResource) { // Not found
next();
return;
Expand Down
Loading