Skip to content

Commit b8ab775

Browse files
committed
[FEATURE] Add MiddlewareUtil providing convenience functions to all middleware
In order to correctly handle requests with URIs containing escape sequences like "%20" (=whitespace), middleware implementations need to decode any escape sequences in incoming request. To make this easier a new MiddlewareUtil class is added. Every MiddlewareManager creates one instance of this class and provides it to every standard middleware. Custom middleware receive an interface to this instance with a set of methods restricted by the specification version of the custom middleware extension. This is yet to be implemented in a follow-up change as the middleware repository does not yet provide specification version information.
1 parent ea4c3fb commit b8ab775

File tree

4 files changed

+211
-15
lines changed

4 files changed

+211
-15
lines changed

lib/middleware/MiddlewareManager.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const middlewareRepository = require("./middlewareRepository");
2+
const MiddlewareUtil = require("./MiddlewareUtil");
23

34
/**
45
*
@@ -18,6 +19,7 @@ class MiddlewareManager {
1819

1920
this.middleware = {};
2021
this.middlewareExecutionOrder = [];
22+
this.middlewareUtil = new MiddlewareUtil();
2123
}
2224

2325
async applyMiddleware(app) {
@@ -70,7 +72,10 @@ class MiddlewareManager {
7072
}
7173

7274
this.middleware[middlewareName] = {
73-
middleware: await Promise.resolve(middlewareCallback({resources: this.resources})),
75+
middleware: await Promise.resolve(middlewareCallback({
76+
resources: this.resources,
77+
middlewareUtil: this.middlewareUtil
78+
})),
7479
mountPath
7580
};
7681
}
@@ -126,9 +131,10 @@ class MiddlewareManager {
126131
await this.addMiddleware("versionInfo", {
127132
mountPath: "/resources/sap-ui-version.json",
128133
wrapperCallback: (versionInfoModule) => {
129-
return ({resources}) => {
134+
return ({resources, middlewareUtil}) => {
130135
return versionInfoModule({
131136
resources,
137+
middlewareUtil,
132138
tree: this.tree
133139
});
134140
};
@@ -142,8 +148,9 @@ class MiddlewareManager {
142148
await this.addMiddleware("nonReadRequests");
143149
await this.addMiddleware("serveIndex", {
144150
wrapperCallback: (middleware) => {
145-
return ({resources}) => middleware({
151+
return ({resources, middlewareUtil}) => middleware({
146152
resources,
153+
middlewareUtil,
147154
simpleIndex: this.options.simpleIndex
148155
});
149156
}
@@ -176,10 +183,13 @@ class MiddlewareManager {
176183

177184
await this.addMiddleware(middlewareDef.name, {
178185
wrapperCallback: (middleware) => {
179-
return ({resources}) => {
186+
return ({resources, middlewareUtil}) => {
180187
const options = {
181188
configuration: middlewareDef.configuration
182189
};
190+
// TODO: Pass "middlewareUtil" only to custom middleware with specVersion >=2.0
191+
// Requires middleware-/task-repository enhancement
192+
// Use middlewareUtil.getInterface(specVersion)
183193
return middleware({resources, options});
184194
};
185195
},

lib/middleware/MiddlewareUtil.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Convenience functions for UI5 Server middleware.
3+
* An instance of this class is passed to every standard UI5 Server middleware.
4+
* Custom middleware that define a specification version >= 2.0 will also receive an instance
5+
* of this class as part of the parameters of their create-middleware function.
6+
*
7+
* The set of functions that can be accessed by a custom middleware depends on the specification
8+
* version defined for the extension.
9+
*
10+
* @public
11+
* @memberof module:@ui5/server.middleware
12+
*/
13+
class MiddlewareUtil {
14+
/**
15+
* Get an interface to an instance of this class that only provides those functions
16+
* that are supported by the given custom middleware extension specification version.
17+
*
18+
* @param {string} specVersion Specification Version of custom middleware extension
19+
* @returns {object} An object with bound instance methods supported by the given specification version
20+
*/
21+
getInterface(specVersion) {
22+
const baseInterface = {
23+
getPathname: this.getPathname.bind(this),
24+
getMimeInfo: this.getMimeInfo.bind(this)
25+
};
26+
switch (specVersion) {
27+
case "0.1":
28+
case "1.0":
29+
case "1.1":
30+
return undefined;
31+
case "2.0":
32+
return baseInterface;
33+
default:
34+
throw new Error(`MiddlewareUtil: Unknown or unsupported specification version ${specVersion}`);
35+
}
36+
}
37+
38+
/**
39+
* Returns the [pathname]{@link https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname}
40+
* of a given request. Any escape sequences will be decoded.
41+
* </br></br>
42+
* This method is only available to custom middleware extensions defining
43+
* <b>specification version 2.0 and above</b>.
44+
*
45+
* @param {object} req Request object
46+
* @returns {string} [Pathname]{@link https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname}
47+
* of the given request
48+
* @public
49+
*/
50+
getPathname(req) {
51+
const parseurl = require("parseurl");
52+
let {pathname} = parseurl(req);
53+
pathname = decodeURIComponent(pathname);
54+
return pathname;
55+
}
56+
57+
/**
58+
* MIME Info
59+
*
60+
* @example
61+
* const mimeInfo = {
62+
* "type": "text/html",
63+
* "charset": "utf-8",
64+
* "contentType": "text/html; charset=utf-8"
65+
* };
66+
*
67+
* @public
68+
* @typedef {object} MimeInfo
69+
* @property {string} type Detected content-type for the given resource path
70+
* @property {string} charset Default charset for the detected content-type
71+
* @property {string} contentType Calculated content-type header value
72+
* @memberof module:@ui5/server.middleware.MiddlewareUtil
73+
*/
74+
/**
75+
* Returns MIME information derived from a given resource path.
76+
* </br></br>
77+
* This method is only available to custom middleware extensions defining
78+
* <b>specification version 2.0 and above</b>.
79+
*
80+
* @param {object} resourcePath
81+
* @returns {module:@ui5/server.middleware.MiddlewareUtil.MimeInfo}
82+
* @public
83+
*/
84+
getMimeInfo(resourcePath) {
85+
const mime = require("mime-types");
86+
const type = mime.lookup(resourcePath) || "application/octet-stream";
87+
const charset = mime.charset(type);
88+
return {
89+
type,
90+
charset,
91+
contentType: type + (charset ? "; charset=" + charset : "")
92+
};
93+
}
94+
}
95+
96+
module.exports = MiddlewareUtil;

lib/mime-util.js

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const test = require("ava");
2+
const sinon = require("sinon");
3+
const mock = require("mock-require");
4+
const MiddlewareUtil = require("../../../../lib/middleware/MiddlewareUtil");
5+
6+
test.afterEach.always((t) => {
7+
sinon.restore();
8+
mock.stopAll();
9+
});
10+
11+
test.serial("getPathname", async (t) => {
12+
const middlewareUtil = new MiddlewareUtil();
13+
const parseurlStub = sinon.stub().returns({pathname: "path%20name"});
14+
mock("parseurl", parseurlStub);
15+
const pathname = middlewareUtil.getPathname("req");
16+
17+
t.is(parseurlStub.callCount, 1, "parseurl got called once");
18+
t.is(parseurlStub.getCall(0).args[0], "req", "parseurl got called with correct argument");
19+
t.is(pathname, "path name", "Correct pathname returned");
20+
});
21+
22+
test.serial("getMimeInfo", async (t) => {
23+
const middlewareUtil = new MiddlewareUtil();
24+
const mime = require("mime-types");
25+
const lookupStub = sinon.stub(mime, "lookup").returns("mytype");
26+
const charsetStub = sinon.stub(mime, "charset").returns("mycharset");
27+
28+
const mimeInfo = middlewareUtil.getMimeInfo("resourcePath");
29+
30+
t.is(lookupStub.callCount, 1, "mime.lookup got called once");
31+
t.is(lookupStub.getCall(0).args[0], "resourcePath", "mime.lookup got called with correct argument");
32+
t.is(charsetStub.callCount, 1, "mime.charset got called once");
33+
t.is(charsetStub.getCall(0).args[0], "mytype", "mime.charset got called with correct argument");
34+
t.deepEqual(mimeInfo, {
35+
type: "mytype",
36+
charset: "mycharset",
37+
contentType: "mytype; charset=mycharset"
38+
}, "Correct pathname returned");
39+
});
40+
41+
test.serial("getMimeInfo: unknown type", async (t) => {
42+
const middlewareUtil = new MiddlewareUtil();
43+
const mime = require("mime-types");
44+
const lookupStub = sinon.stub(mime, "lookup");
45+
const charsetStub = sinon.stub(mime, "charset");
46+
47+
const mimeInfo = middlewareUtil.getMimeInfo("resourcePath");
48+
49+
t.is(lookupStub.callCount, 1, "mime.lookup got called once");
50+
t.is(lookupStub.getCall(0).args[0], "resourcePath", "mime.lookup got called with correct argument");
51+
t.is(charsetStub.callCount, 1, "mime.charset got called once");
52+
t.is(charsetStub.getCall(0).args[0], "application/octet-stream", "mime.charset got called with correct argument");
53+
t.deepEqual(mimeInfo, {
54+
type: "application/octet-stream",
55+
charset: undefined,
56+
contentType: "application/octet-stream"
57+
}, "Correct pathname returned");
58+
});
59+
60+
test("getInterface: specVersion 1.0", async (t) => {
61+
const middlewareUtil = new MiddlewareUtil();
62+
63+
const interfacedMiddlewareUtil = middlewareUtil.getInterface("1.0");
64+
65+
t.is(interfacedMiddlewareUtil, undefined, "no interface provided");
66+
});
67+
68+
test("getInterface: specVersion 2.0", async (t) => {
69+
const middlewareUtil = new MiddlewareUtil();
70+
71+
const interfacedMiddlewareUtil = middlewareUtil.getInterface("2.0");
72+
73+
t.deepEqual(Object.keys(interfacedMiddlewareUtil), [
74+
"getPathname",
75+
"getMimeInfo"
76+
], "Correct methods are provided");
77+
78+
t.is(typeof interfacedMiddlewareUtil.getPathname, "function", "function getPathname is provided");
79+
t.is(typeof interfacedMiddlewareUtil.getMimeInfo, "function", "function getMimeInfo is provided");
80+
});
81+
82+
test("getInterface: specVersion undefined", async (t) => {
83+
const middlewareUtil = new MiddlewareUtil();
84+
85+
const err = t.throws(() => {
86+
middlewareUtil.getInterface();
87+
});
88+
89+
t.is(err.message, "MiddlewareUtil: Unknown or unsupported specification version undefined",
90+
"Throw with correct error message");
91+
});
92+
93+
test("getInterface: specVersion unknown", async (t) => {
94+
const middlewareUtil = new MiddlewareUtil();
95+
const err = t.throws(() => {
96+
middlewareUtil.getInterface("1.5");
97+
});
98+
99+
t.is(err.message, "MiddlewareUtil: Unknown or unsupported specification version 1.5",
100+
"Throw with correct error message");
101+
});

0 commit comments

Comments
 (0)