Skip to content

Commit 6156f13

Browse files
committed
[POC] Proxy mode
1 parent dc51139 commit 6156f13

12 files changed

+592
-29
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: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const middlewareRepository = require("./middlewareRepository");
2+
const proxyConfiguration = require("../proxyConfiguration");
23

34
/**
45
*
@@ -7,7 +8,8 @@ const middlewareRepository = require("./middlewareRepository");
78
*/
89
class MiddlewareManager {
910
constructor({tree, resources, options = {
10-
sendSAPTargetCSP: false
11+
sendSAPTargetCSP: false,
12+
useProxy: false
1113
}}) {
1214
if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) {
1315
throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided");
@@ -76,6 +78,13 @@ class MiddlewareManager {
7678
}
7779

7880
async addStandardMiddleware() {
81+
const useProxy = this.options.useProxy;
82+
83+
let proxyConfig;
84+
if (useProxy) {
85+
proxyConfig = await proxyConfiguration.getConfigurationForProject(this.tree);
86+
}
87+
7988
await this.addMiddleware("csp", {
8089
wrapperCallback: (cspModule) => {
8190
const oCspConfig = {
@@ -117,6 +126,21 @@ class MiddlewareManager {
117126
});
118127
await this.addMiddleware("compression");
119128
await this.addMiddleware("cors");
129+
130+
if (useProxy) {
131+
await this.addMiddleware("proxyRewrite", {
132+
wrapperCallback: (proxyRewriteModule) => {
133+
return ({resources}) => {
134+
return proxyRewriteModule({
135+
resources,
136+
configuration: proxyConfig,
137+
cdnUrl: this.options.cdnUrl
138+
});
139+
};
140+
}
141+
});
142+
}
143+
120144
await this.addMiddleware("discovery", {
121145
mountPath: "/discovery"
122146
});
@@ -133,20 +157,52 @@ class MiddlewareManager {
133157
};
134158
}
135159
});
136-
await this.addMiddleware("connectUi5Proxy", {
137-
mountPath: "/proxy"
138-
});
160+
161+
if (this.options.cdnUrl) {
162+
await this.addMiddleware("cdn", {
163+
wrapperCallback: (cdn) => {
164+
return ({resources}) => {
165+
return cdn({
166+
resources,
167+
cdnUrl: this.options.cdnUrl
168+
});
169+
};
170+
}
171+
});
172+
}
173+
174+
if (useProxy) {
175+
await this.addMiddleware("proxy", {
176+
wrapperCallback: (proxyModule) => {
177+
return ({resources}) => {
178+
return proxyModule({
179+
resources,
180+
configuration: proxyConfig
181+
});
182+
};
183+
}
184+
});
185+
} else {
186+
await this.addMiddleware("connectUi5Proxy", {
187+
mountPath: "/proxy"
188+
});
189+
}
190+
139191
// Handle anything but read operations *before* the serveIndex middleware
140192
// as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling
141193
await this.addMiddleware("nonReadRequests");
142-
await this.addMiddleware("serveIndex", {
143-
wrapperCallback: (middleware) => {
144-
return ({resources}) => middleware({
145-
resources,
146-
simpleIndex: this.options.simpleIndex
147-
});
148-
}
149-
});
194+
195+
if (!useProxy) {
196+
// Don't do directory listing when using a proxy. High potential for confusion
197+
await this.addMiddleware("serveIndex", {
198+
wrapperCallback: (middleware) => {
199+
return ({resources}) => middleware({
200+
resources,
201+
simpleIndex: this.options.simpleIndex
202+
});
203+
}
204+
});
205+
}
150206
}
151207

152208
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)