Skip to content

Commit b137c12

Browse files
committed
[POC] Proxy mode
1 parent 0d18f0e commit b137c12

12 files changed

+544
-37
lines changed

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const modules = {
66
server: "./lib/server",
77
sslUtil: "./lib/sslUtil",
8+
proxyConfiguration: "./lib/proxyConfiguration",
89
middlewareRepository: "./lib/middleware/middlewareRepository"
910
};
1011

lib/middleware/MiddlewareManager.js

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const middlewareRepository = require("./middlewareRepository");
22
const MiddlewareUtil = require("./MiddlewareUtil");
3+
const proxyConfiguration = require("../proxyConfiguration");
34

45
/**
56
*
@@ -8,7 +9,8 @@ const MiddlewareUtil = require("./MiddlewareUtil");
89
*/
910
class MiddlewareManager {
1011
constructor({tree, resources, options = {
11-
sendSAPTargetCSP: false
12+
sendSAPTargetCSP: false,
13+
useProxy: false
1214
}}) {
1315
if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) {
1416
throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided");
@@ -84,6 +86,13 @@ class MiddlewareManager {
8486
}
8587

8688
async addStandardMiddleware() {
89+
const useProxy = this.options.useProxy;
90+
91+
let proxyConfig;
92+
if (useProxy) {
93+
proxyConfig = await proxyConfiguration.getConfigurationForProject(this.tree);
94+
}
95+
8796
await this.addMiddleware("csp", {
8897
wrapperCallback: ({middleware: cspModule}) => {
8998
const oCspConfig = {
@@ -125,6 +134,21 @@ class MiddlewareManager {
125134
});
126135
await this.addMiddleware("compression");
127136
await this.addMiddleware("cors");
137+
138+
if (useProxy) {
139+
await this.addMiddleware("proxyRewrite", {
140+
wrapperCallback: (proxyRewriteModule) => {
141+
return ({resources}) => {
142+
return proxyRewriteModule({
143+
resources,
144+
configuration: proxyConfig,
145+
cdnUrl: this.options.cdnUrl
146+
});
147+
};
148+
}
149+
});
150+
}
151+
128152
await this.addMiddleware("discovery", {
129153
mountPath: "/discovery"
130154
});
@@ -143,21 +167,52 @@ class MiddlewareManager {
143167
};
144168
}
145169
});
146-
await this.addMiddleware("connectUi5Proxy", {
147-
mountPath: "/proxy"
148-
});
170+
171+
if (this.options.cdnUrl) {
172+
await this.addMiddleware("cdn", {
173+
wrapperCallback: (cdn) => {
174+
return ({resources}) => {
175+
return cdn({
176+
resources,
177+
cdnUrl: this.options.cdnUrl
178+
});
179+
};
180+
}
181+
});
182+
}
183+
184+
if (useProxy) {
185+
await this.addMiddleware("proxy", {
186+
wrapperCallback: (proxyModule) => {
187+
return ({resources}) => {
188+
return proxyModule({
189+
resources,
190+
configuration: proxyConfig
191+
});
192+
};
193+
}
194+
});
195+
} else {
196+
await this.addMiddleware("connectUi5Proxy", {
197+
mountPath: "/proxy"
198+
});
199+
}
200+
149201
// Handle anything but read operations *before* the serveIndex middleware
150202
// as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling
151203
await this.addMiddleware("nonReadRequests");
152-
await this.addMiddleware("serveIndex", {
153-
wrapperCallback: ({middleware: middleware}) => {
154-
return ({resources, middlewareUtil}) => middleware({
155-
resources,
156-
middlewareUtil,
157-
simpleIndex: this.options.simpleIndex
158-
});
159-
}
160-
});
204+
if (!useProxy) {
205+
// Don't do directory listing when using a proxy. High potential for confusion
206+
await this.addMiddleware("serveIndex", {
207+
wrapperCallback: ({middleware: middleware}) => {
208+
return ({resources, middlewareUtil}) => middleware({
209+
resources,
210+
middlewareUtil,
211+
simpleIndex: this.options.simpleIndex
212+
});
213+
}
214+
});
215+
}
161216
}
162217

163218
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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ const middlewareInfos = {
99
connectUi5Proxy: {path: "./connectUi5Proxy"},
1010
serveThemes: {path: "./serveThemes"},
1111
testRunner: {path: "./testRunner"},
12-
nonReadRequests: {path: "./nonReadRequests"}
12+
nonReadRequests: {path: "./nonReadRequests"},
13+
proxy: "./proxy",
14+
proxyRewrite: "./proxyRewrite"
1315
};
1416

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