Skip to content

Commit 037b3bc

Browse files
authored
[FEATURE] Server: Add handling for custom middleware (#200)
As per RFC 0005: SAP/ui5-tooling#151
1 parent 14571e2 commit 037b3bc

15 files changed

+870
-126
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
## Server
1616
Provides server capabilities for the [UI5 Tooling](https://github.com/SAP/ui5-tooling).
1717

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

2121
* Translation files with `.properties` extension are properly encoded with **ISO-8859-1**.
2222
* Changes on files with `.less` extension triggers a theme build and delivers the compiled CSS files.

index.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,33 @@
55
module.exports = {
66
server: require("./lib/server"),
77
sslUtil: require("./lib/sslUtil"),
8+
middlewareRepository: require("./lib/middleware/middlewareRepository"),
9+
10+
// Legacy middleware export. Still private.
811
middleware: {
912
csp: require("./lib/middleware/csp"),
10-
discovery: require("./lib/middleware/discovery"),
11-
nonReadRequests: require("./lib/middleware/discovery"),
12-
serveIndex: require("./lib/middleware/serveIndex"),
13-
serveResources: require("./lib/middleware/serveResources"),
14-
serveThemes: require("./lib/middleware/serveThemes"),
15-
versionInfo: require("./lib/middleware/versionInfo"),
13+
discovery: mapLegacyMiddlewareArguments(require("./lib/middleware/discovery")),
14+
nonReadRequests: mapLegacyMiddlewareArguments(require("./lib/middleware/discovery")),
15+
serveIndex: mapLegacyMiddlewareArguments(require("./lib/middleware/serveIndex")),
16+
serveResources: mapLegacyMiddlewareArguments(require("./lib/middleware/serveResources")),
17+
serveThemes: mapLegacyMiddlewareArguments(require("./lib/middleware/serveThemes")),
18+
versionInfo: mapLegacyMiddlewareArguments(require("./lib/middleware/versionInfo")),
1619
}
1720
};
21+
22+
function mapLegacyMiddlewareArguments(module) {
23+
// Old arguments was a single object with optional properties
24+
// - resourceCollections
25+
// - tree
26+
return function({resourceCollections, tree} = {}) {
27+
const resources = {};
28+
resources.all = resourceCollections.combo;
29+
resources.rootProject = resourceCollections.source;
30+
resources.dependencies = resourceCollections.dependencies;
31+
32+
return module({
33+
resources,
34+
tree
35+
});
36+
};
37+
}

lib/middleware/MiddlewareManager.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const middlewareRepository = require("./middlewareRepository");
2+
/**
3+
*
4+
*
5+
* @memberof module:@ui5/server.middleware
6+
*/
7+
class MiddlewareManager {
8+
constructor({tree, resources, options = {
9+
sendSAPTargetCSP: false
10+
}}) {
11+
if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) {
12+
throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided");
13+
}
14+
this.tree = tree;
15+
this.resources = resources;
16+
this.options = options;
17+
18+
this.middleware = {};
19+
this.middlewareExecutionOrder = [];
20+
}
21+
22+
async applyMiddleware(app) {
23+
await this.addStandardMiddleware();
24+
await this.addCustomMiddleware();
25+
26+
return this.middlewareExecutionOrder.map((name) => {
27+
const m = this.middleware[name];
28+
app.use(m.mountPath, m.middleware);
29+
});
30+
}
31+
32+
async addMiddleware(middlewareName, {
33+
wrapperCallback, mountPath = "/",
34+
beforeMiddleware, afterMiddleware
35+
} = {}) {
36+
let middlewareCallback = middlewareRepository.getMiddleware(middlewareName);
37+
if (wrapperCallback) {
38+
middlewareCallback = wrapperCallback(middlewareCallback);
39+
}
40+
if (this.middleware[middlewareName] || this.middlewareExecutionOrder.includes(middlewareName)) {
41+
throw new Error(`Failed to add duplicate middleware ${middlewareName}`);
42+
}
43+
44+
if (beforeMiddleware || afterMiddleware) {
45+
const refMiddlewareName = beforeMiddleware || afterMiddleware;
46+
let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName);
47+
if (refMiddlewareIdx === -1) {
48+
throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` +
49+
`middleware ${middlewareName}`);
50+
}
51+
if (afterMiddleware) {
52+
// Insert after index of referenced middleware
53+
refMiddlewareIdx++;
54+
}
55+
this.middlewareExecutionOrder.splice(refMiddlewareIdx, 0, middlewareName);
56+
} else {
57+
this.middlewareExecutionOrder.push(middlewareName);
58+
}
59+
60+
this.middleware[middlewareName] = {
61+
middleware: await Promise.resolve(middlewareCallback({resources: this.resources})),
62+
mountPath
63+
};
64+
}
65+
66+
async addStandardMiddleware() {
67+
await this.addMiddleware("csp", {
68+
wrapperCallback: (cspModule) => {
69+
const oCspConfig = {
70+
allowDynamicPolicySelection: true,
71+
allowDynamicPolicyDefinition: true,
72+
definedPolicies: {
73+
"sap-target-level-1":
74+
"default-src 'self'; " +
75+
"script-src 'self' 'unsafe-eval'; " +
76+
"style-src 'self' 'unsafe-inline'; " +
77+
"font-src 'self' data:; " +
78+
"img-src 'self' * data: blob:; " +
79+
"frame-src 'self' https: data: blob:; " +
80+
"child-src 'self' https: data: blob:; " +
81+
"connect-src 'self' https: wss:;",
82+
"sap-target-level-2":
83+
"default-src 'self'; " +
84+
"script-src 'self'; " +
85+
"style-src 'self' 'unsafe-inline'; " +
86+
"font-src 'self' data:; " +
87+
"img-src 'self' * data: blob:; " +
88+
"frame-src 'self' https: data: blob:; " +
89+
"child-src 'self' https: data: blob:; " +
90+
"connect-src 'self' https: wss:;"
91+
}
92+
};
93+
if (this.options.sendSAPTargetCSP) {
94+
Object.assign(oCspConfig, {
95+
defaultPolicy: "sap-target-level-1",
96+
defaultPolicyIsReportOnly: true,
97+
defaultPolicy2: "sap-target-level-2",
98+
defaultPolicy2IsReportOnly: true,
99+
});
100+
}
101+
return () => {
102+
return cspModule("sap-ui-xx-csp-policy", oCspConfig);
103+
};
104+
}
105+
});
106+
await this.addMiddleware("compression");
107+
await this.addMiddleware("cors");
108+
await this.addMiddleware("discovery", {
109+
mountPath: "/discovery"
110+
});
111+
await this.addMiddleware("serveResources");
112+
await this.addMiddleware("serveThemes");
113+
await this.addMiddleware("versionInfo", {
114+
mountPath: "/resources/sap-ui-version.json",
115+
wrapperCallback: (versionInfoModule) => {
116+
return ({resources}) => {
117+
return versionInfoModule({
118+
resources,
119+
tree: this.tree
120+
});
121+
};
122+
}
123+
});
124+
await this.addMiddleware("connectUi5Proxy", {
125+
mountPath: "/proxy"
126+
});
127+
// Handle anything but read operations *before* the serveIndex middleware
128+
// as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling
129+
await this.addMiddleware("nonReadRequests");
130+
await this.addMiddleware("serveIndex");
131+
}
132+
133+
async addCustomMiddleware() {
134+
const project = this.tree;
135+
const projectCustomMiddleware = project.server && project.server.customMiddleware;
136+
if (!projectCustomMiddleware || projectCustomMiddleware.length === 0) {
137+
return; // No custom middleware defined
138+
}
139+
140+
for (let i = 0; i < projectCustomMiddleware.length; i++) {
141+
const middlewareDef = projectCustomMiddleware[i];
142+
if (!middlewareDef.name) {
143+
throw new Error(`Missing name for custom middleware definition of project ${project.metadata.name} ` +
144+
`at index ${i}`);
145+
}
146+
if (middlewareDef.beforeMiddleware && middlewareDef.afterMiddleware) {
147+
throw new Error(
148+
`Custom middleware definition ${middlewareDef.name} of project ${project.metadata.name} ` +
149+
`defines both "beforeMiddleware" and "afterMiddleware" parameters. Only one must be defined.`);
150+
}
151+
if (!middlewareDef.beforeMiddleware && !middlewareDef.afterMiddleware) {
152+
throw new Error(
153+
`Custom middleware definition ${middlewareDef.name} of project ${project.metadata.name} ` +
154+
`defines neither a "beforeMiddleware" nor an "afterMiddleware" parameter. One must be defined.`);
155+
}
156+
157+
if (this.middleware[middlewareDef.name]) {
158+
// Middleware is already known
159+
throw new Error(`Failed to add custom middleware ${middlewareDef.name}. ` +
160+
`A middleware with the same name is already known.`);
161+
}
162+
await this.addMiddleware(middlewareDef.name, {
163+
wrapperCallback: (middleware) => {
164+
return ({resources}) => {
165+
const options = {
166+
configuration: middlewareDef.configuration
167+
};
168+
return middleware({resources, options});
169+
};
170+
},
171+
mountPath: middlewareDef.mountPath,
172+
beforeMiddleware: middlewareDef.beforeMiddleware,
173+
afterMiddleware: middlewareDef.afterMiddleware
174+
});
175+
}
176+
}
177+
}
178+
module.exports = MiddlewareManager;

lib/middleware/connectUi5Proxy.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const ui5connect = require("connect-openui5");
2+
3+
function createMiddleware() {
4+
return ui5connect.proxy({
5+
secure: false
6+
});
7+
}
8+
9+
module.exports = createMiddleware;

lib/middleware/discovery.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ const urlPattern = /\/(app_pages|all_libs|all_tests)(?:[?#].*)?$/;
1313
* </ul>
1414
*
1515
* @module @ui5/server/middleware/discovery
16-
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
17-
* @param {module:@ui5/fs.AbstractReader} resourceCollections.source Resource reader or collection for the source project
18-
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
16+
* @param {Object} parameters Parameters
17+
* @param {module:@ui5/fs.AbstractReader} parameters.resources.all Reader or Collection to read resources of the
18+
* root project and its dependencies
19+
* @param {module:@ui5/fs.AbstractReader} parameters.resources.rootProject Reader or Collection to read resources of
20+
* the project the server is started in
1921
* @returns {Function} Returns a server middleware closure.
2022
*/
21-
function createMiddleware({resourceCollections}) {
23+
function createMiddleware({resources}) {
2224
return function discoveryMiddleware(req, res, next) {
2325
const parts = urlPattern.exec(req.url);
2426
const type = parts && parts[1];
@@ -46,7 +48,7 @@ function createMiddleware({resourceCollections}) {
4648
}
4749

4850
if (type === "app_pages") {
49-
resourceCollections.source.byGlob("/**/*.{html,htm}").then(function(resources) {
51+
resources.rootProject.byGlob("/**/*.{html,htm}").then(function(resources) {
5052
resources.forEach(function(resource) {
5153
const relPath = resource.getPath().substr(1); // cut off leading "/"
5254
response.push({
@@ -56,7 +58,7 @@ function createMiddleware({resourceCollections}) {
5658
sendResponse();
5759
});
5860
} else if (type === "all_libs") {
59-
resourceCollections.combo.byGlob([
61+
resources.all.byGlob([
6062
"/resources/**/*.library"
6163
]).then(function(resources) {
6264
resources.forEach(function(resource) {
@@ -72,8 +74,8 @@ function createMiddleware({resourceCollections}) {
7274
});
7375
} else if (type === "all_tests") {
7476
Promise.all([
75-
resourceCollections.combo.byGlob("/resources/**/*.library"),
76-
resourceCollections.combo.byGlob("/test-resources/**/*.{html,htm}")
77+
resources.all.byGlob("/resources/**/*.library"),
78+
resources.all.byGlob("/test-resources/**/*.{html,htm}")
7779
]).then(function(results) {
7880
const libraryResources = results[0];
7981
const testPageResources = results[1];
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const middlewares = {
2+
compression: "compression",
3+
cors: "cors",
4+
csp: "./csp",
5+
serveResources: "./serveResources",
6+
serveIndex: "./serveIndex",
7+
discovery: "./discovery",
8+
versionInfo: "./versionInfo",
9+
connectUi5Proxy: "./connectUi5Proxy",
10+
serveThemes: "./serveThemes",
11+
nonReadRequests: "./nonReadRequests"
12+
};
13+
14+
function getMiddleware(middlewareName) {
15+
const middlewarePath = middlewares[middlewareName];
16+
17+
if (!middlewarePath) {
18+
throw new Error(`middlewareRepository: Unknown Middleware ${middlewareName}`);
19+
}
20+
return require(middlewarePath);
21+
}
22+
23+
function addMiddleware(name, middlewarePath) {
24+
if (middlewares[name]) {
25+
throw new Error(`middlewareRepository: Middleware ${name} already registered`);
26+
}
27+
middlewares[name] = middlewarePath;
28+
}
29+
30+
module.exports = {
31+
getMiddleware: getMiddleware,
32+
addMiddleware: addMiddleware
33+
};

lib/middleware/serveIndex.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,16 @@ function createContent(path, resourceInfos) {
133133
* Creates and returns the middleware to serve a resource index.
134134
*
135135
* @module @ui5/server/middleware/serveIndex
136-
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
137-
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
136+
* @param {Object} resources Contains the resource reader or collection to access project related files
137+
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
138138
* @returns {Function} Returns a server middleware closure.
139139
*/
140-
function createMiddleware({resourceCollections}) {
140+
function createMiddleware({resources}) {
141141
return function serveIndex(req, res, next) {
142142
const pathname = parseurl(req).pathname;
143143
log.verbose("\n Listing index of " + pathname);
144144
const glob = pathname + (pathname.endsWith("/") ? "*" : "/*");
145-
resourceCollections.combo.byGlob(glob, {nodir: false}).then((resources) => {
145+
resources.all.byGlob(glob, {nodir: false}).then((resources) => {
146146
if (!resources || resources.length == 0) { // Not found
147147
next();
148148
return;

lib/middleware/serveResources.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ function isFresh(req, res) {
1919
* Creates and returns the middleware to serve application resources.
2020
*
2121
* @module @ui5/server/middleware/serveResources
22-
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
23-
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
22+
* @param {Object} resources Contains the resource reader or collection to access project related files
23+
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
2424
* @returns {Function} Returns a server middleware closure.
2525
*/
26-
function createMiddleware({resourceCollections}) {
26+
function createMiddleware({resources}) {
2727
return function serveResources(req, res, next) {
2828
const pathname = parseurl(req).pathname;
29-
resourceCollections.combo.byPath(pathname).then(function(resource) {
29+
resources.all.byPath(pathname).then(function(resource) {
3030
if (!resource) { // Not found
3131
next();
3232
return;

lib/middleware/serveThemes.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ const themeRequest = /^(.*\/)library(?:(\.css)|(-RTL\.css)|(-parameters\.json))$
1818
* The theme is built in realtime. If a less file was modified, the theme build is triggered to rebuild the theme.
1919
*
2020
* @module @ui5/server/middleware/serveThemes
21-
* @param {Object} resourceCollections Contains the resource reader or collection to access project related files
22-
* @param {module:@ui5/fs.AbstractReader} resourceCollections.combo Resource collection which contains the workspace and the project dependencies
21+
* @param {Object} resources Contains the resource reader or collection to access project related files
22+
* @param {module:@ui5/fs.AbstractReader} resources.all Resource collection which contains the workspace and the project dependencies
2323
* @returns {Function} Returns a server middleware closure.
2424
*/
25-
function createMiddleware({resourceCollections}) {
25+
function createMiddleware({resources}) {
2626
const builder = new themeBuilder.ThemeBuilder({
27-
fs: fsInterface(resourceCollections.combo)
27+
fs: fsInterface(resources.all)
2828
});
2929

3030
return function theme(req, res, next) {
@@ -46,7 +46,7 @@ function createMiddleware({resourceCollections}) {
4646
}
4747

4848
const sourceLessPath = themeReq[1] + "library.source.less";
49-
resourceCollections.combo.byPath(sourceLessPath).then((sourceLessResource) => {
49+
resources.all.byPath(sourceLessPath).then((sourceLessResource) => {
5050
if (!sourceLessResource) { // Not found
5151
next();
5252
return;

0 commit comments

Comments
 (0)