Skip to content

Commit 9317a77

Browse files
committed
[POC] Proxy mode
1 parent d02d828 commit 9317a77

12 files changed

+589
-23
lines changed

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
module.exports = {
66
server: require("./lib/server"),
77
sslUtil: require("./lib/sslUtil"),
8+
proxyConfiguration: require("./lib/proxyConfiguration"),
89
middlewareRepository: require("./lib/middleware/middlewareRepository"),
910

1011
// Legacy middleware export. Still private.

lib/middleware/MiddlewareManager.js

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
const middlewareRepository = require("./middlewareRepository");
2+
const proxyConfiguration = require("../proxyConfiguration");
3+
24
/**
35
*
46
*
57
* @memberof module:@ui5/server.middleware
68
*/
79
class MiddlewareManager {
810
constructor({tree, resources, options = {
9-
sendSAPTargetCSP: false
11+
sendSAPTargetCSP: false,
12+
useProxy: false
1013
}}) {
1114
if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) {
1215
throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided");
@@ -64,6 +67,13 @@ class MiddlewareManager {
6467
}
6568

6669
async addStandardMiddleware() {
70+
const useProxy = this.options.useProxy;
71+
72+
let proxyConfig;
73+
if (useProxy) {
74+
proxyConfig = await proxyConfiguration.getConfigurationForProject(this.tree);
75+
}
76+
6777
await this.addMiddleware("csp", {
6878
wrapperCallback: (cspModule) => {
6979
const oCspConfig = {
@@ -105,6 +115,21 @@ class MiddlewareManager {
105115
});
106116
await this.addMiddleware("compression");
107117
await this.addMiddleware("cors");
118+
119+
if (useProxy) {
120+
await this.addMiddleware("proxyRewrite", {
121+
wrapperCallback: (proxyRewriteModule) => {
122+
return ({resources}) => {
123+
return proxyRewriteModule({
124+
resources,
125+
configuration: proxyConfig,
126+
cdnUrl: this.options.cdnUrl
127+
});
128+
};
129+
}
130+
});
131+
}
132+
108133
await this.addMiddleware("discovery", {
109134
mountPath: "/discovery"
110135
});
@@ -121,13 +146,44 @@ class MiddlewareManager {
121146
};
122147
}
123148
});
124-
await this.addMiddleware("connectUi5Proxy", {
125-
mountPath: "/proxy"
126-
});
149+
150+
if (this.options.cdnUrl) {
151+
await this.addMiddleware("cdn", {
152+
wrapperCallback: (cdn) => {
153+
return ({resources}) => {
154+
return cdn({
155+
resources,
156+
cdnUrl: this.options.cdnUrl
157+
});
158+
};
159+
}
160+
});
161+
}
162+
163+
if (useProxy) {
164+
await this.addMiddleware("proxy", {
165+
wrapperCallback: (proxyModule) => {
166+
return ({resources}) => {
167+
return proxyModule({
168+
resources,
169+
configuration: proxyConfig
170+
});
171+
};
172+
}
173+
});
174+
} else {
175+
await this.addMiddleware("connectUi5Proxy", {
176+
mountPath: "/proxy"
177+
});
178+
}
179+
127180
// Handle anything but read operations *before* the serveIndex middleware
128181
// as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling
129182
await this.addMiddleware("nonReadRequests");
130-
await this.addMiddleware("serveIndex");
183+
if (!useProxy) {
184+
// Don't do directory listing when using a proxy. High potential for confusion
185+
await this.addMiddleware("serveIndex");
186+
}
131187
}
132188

133189
async addCustomMiddleware() {

lib/middleware/cdn.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const log = require("@ui5/logger").getLogger("server:middleware:cdn");
2+
const http = require("http");
3+
const https = require("https");
4+
5+
function createMiddleware({cdnUrl}) {
6+
if (!cdnUrl) {
7+
throw new Error(`Missing parameter "cdnUrl"`);
8+
}
9+
if (cdnUrl.endsWith("/")) {
10+
throw new Error(`Parameter "cdnUrl" must not end with a slash`);
11+
}
12+
13+
return function proxy(req, res, next) {
14+
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS") {
15+
// Cannot be fulfilled by CDN
16+
next();
17+
return;
18+
}
19+
20+
log.verbose(`Requesting ${req.url} from CDN ${cdnUrl}...`);
21+
log.verbose(`Orig. URL: ${req.originalUrl}`);
22+
23+
getResource({
24+
cdnUrl,
25+
resourcePath: req.url,
26+
resolveOnOddStatusCode: true,
27+
headers: req.headers
28+
}).then(({data, headers, statusCode}) => {
29+
if (statusCode !== 200) {
30+
// odd status code
31+
log.verbose(`CDN replied with status code ${statusCode} for request ${req.url}`);
32+
next();
33+
return;
34+
}
35+
if (headers) {
36+
for (const headerKey in headers) {
37+
if (headers.hasOwnProperty(headerKey)) {
38+
res.setHeader(headerKey, headers[headerKey]);
39+
}
40+
}
41+
}
42+
43+
res.setHeader("x-ui5-tooling-proxied-from-cdn", cdnUrl);
44+
res.setHeader("x-ui5-tooling-proxied-as", req.url);
45+
46+
res.send(data);
47+
}).catch((err) => {
48+
log.error(`CDN request error: ${err.message}`);
49+
next(err);
50+
});
51+
};
52+
}
53+
54+
const cache = {};
55+
56+
function getResource({cdnUrl, resourcePath, resolveOnOddStatusCode, headers}) {
57+
return new Promise((resolve, reject) => {
58+
const reqUrl = cdnUrl + resourcePath;
59+
if (cache[reqUrl]) {
60+
resolve(cache[reqUrl]);
61+
}
62+
if (!cdnUrl.startsWith("http")) {
63+
throw new Error(`CDN URL must start with protocol "http" or "https": ${cdnUrl}`);
64+
}
65+
let client = http;
66+
if (cdnUrl.startsWith("https")) {
67+
client = https;
68+
}
69+
client.get(reqUrl, (cdnResponse) => {
70+
const {statusCode} = cdnResponse;
71+
72+
const data = [];
73+
cdnResponse.on("data", (chunk) => {
74+
data.push(chunk);
75+
});
76+
cdnResponse.on("end", () => {
77+
try {
78+
const result = {
79+
data: Buffer.concat(data),
80+
statusCode,
81+
headers: cdnResponse.headers
82+
};
83+
cache[reqUrl] = result;
84+
if (Object.keys(cache).length % 10 === 0) {
85+
log.verbose(`Cache size: ${Object.keys(cache).length} entries`);
86+
}
87+
resolve(result);
88+
} catch (err) {
89+
reject(err);
90+
}
91+
});
92+
}).on("error", (err) => {
93+
reject(err);
94+
});
95+
});
96+
}
97+
98+
module.exports = createMiddleware;
99+
module.exports.getResource = getResource;

lib/middleware/middlewareRepository.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ const middlewares = {
22
compression: "compression",
33
cors: "cors",
44
csp: "./csp",
5+
cdn: "./cdn",
56
serveResources: "./serveResources",
67
serveIndex: "./serveIndex",
78
discovery: "./discovery",
89
versionInfo: "./versionInfo",
910
connectUi5Proxy: "./connectUi5Proxy",
1011
serveThemes: "./serveThemes",
11-
nonReadRequests: "./nonReadRequests"
12+
nonReadRequests: "./nonReadRequests",
13+
proxy: "./proxy",
14+
proxyRewrite: "./proxyRewrite"
1215
};
1316

1417
function getMiddleware(middlewareName) {

lib/middleware/proxy.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const log = require("@ui5/logger").getLogger("server:middleware:proxy");
2+
const httpProxy = require("http-proxy");
3+
4+
function createMiddleware({configuration}) {
5+
let agent;
6+
7+
if (configuration.forwardProxy) {
8+
let username = configuration.forwardProxy.username;
9+
let password = configuration.forwardProxy.password;
10+
if (!username) {
11+
// TODO prompt user for credentials
12+
username = "";
13+
}
14+
if (!password) {
15+
// TODO prompt user for credentials
16+
password = "";
17+
}
18+
const HttpsProxyAgent = require("https-proxy-agent");
19+
agent = new HttpsProxyAgent({
20+
host: configuration.forwardProxy.hostname,
21+
port: configuration.forwardProxy.port,
22+
secureProxy: configuration.forwardProxy.useSsl,
23+
auth: username + ":" + password
24+
});
25+
}
26+
27+
const proxyServer = httpProxy.createProxyServer({
28+
agent: agent,
29+
secure: !configuration.insecure,
30+
prependPath: false,
31+
xfwd: true,
32+
target: configuration.destination.origin,
33+
changeOrigin: true
34+
});
35+
36+
proxyServer.on("proxyRes", function(proxyRes, req, res) {
37+
res.setHeader("x-ui5-tooling-proxied-from", configuration.destination.origin);
38+
});
39+
40+
return function proxy(req, res, next) {
41+
if (req.url !== req.originalUrl) {
42+
log.verbose(`Proxying "${req.url}"`); // normalized URL - used for local resolution
43+
log.verbose(` as "${req.originalUrl}"`); // original URL - used for reverse proxy requests
44+
} else {
45+
log.verbose(`Proxying "${req.url}"`);
46+
}
47+
req.url = req.originalUrl; // Always use the original (non-rewritten) URL
48+
proxyServer.web(req, res, (err) => {
49+
log.error(`Proxy error: ${err.message}`);
50+
next(err);
51+
});
52+
};
53+
}
54+
55+
module.exports = createMiddleware;

0 commit comments

Comments
 (0)