2025-01-20 10:32:22 +09:00

523 lines
15 KiB
JavaScript

const test = require("ava");
const express = require("express");
const { createProxyMiddleware, responseInterceptor } = require("http-proxy-middleware");
const expressws = require("ui5-middleware-websocket/lib/expressws");
const querystring = require("querystring");
const supertest = require("supertest");
const superwstest = require("superwstest");
const nock = require("nock");
const proxy = require("node-tcp-proxy");
let proxyServerHitCount = 0;
// Start server before running tests
test.before(async (t) => {
// create the ports for the proxy server
const getPort = (await import("get-port")).default;
const proxyServerPort = await getPort();
const wsServerPort = await getPort();
const wsProxyServerPort = await getPort();
// define environment variables
//process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
process.env.HTTP_PROXY = `http://localhost:${proxyServerPort}`;
process.env.NO_PROXY = `secure.example.com,localhost`;
// setup the corporate proxy server (simulate close function)
const proxyServer = (t.context.proxyServer = proxy.createProxy(proxyServerPort, "corporate-proxy", 80, {
upstream: function (context, data) {
//const { remoteAddress, remotePort } = context.proxySocket;
//console.log(`[PROXY] Client ${remoteAddress}:${remotePort}: ${data}`);
proxyServerHitCount++;
// do something with the data and return modified data
return data;
},
downstream: function (context, data) {
//const { remoteAddress, remotePort } = context.serviceSocket;
//console.log(`[PROXY] Service ${remoteAddress}:${remotePort}: ${data}`);
// do something with the data and return modified data
return data;
},
}));
t.context.proxyServer.close = function (cb) {
proxyServer.end();
cb();
};
// setup the WebSocket echo server (as counterpart)
const wsApp = express();
expressws(wsApp); // enhance the express app for websocket support
wsApp.ws("/", function (ws /*, req, next */) {
ws.on("message", function (message) {
console.log(`message: ${message}`);
ws.send(`echo ${message}`);
});
ws.send(`hello`);
});
t.context.wsServer = wsApp.listen(wsServerPort);
// setup a standard express proxy server using the default
// http-proxy-middleware plus the WebSocket extension
const app = express();
const wsProxy = createProxyMiddleware({
target: `ws://localhost:${wsServerPort}`,
changeOrigin: true,
autoRewrite: true,
xfwd: true,
secure: false,
pathRewrite: {
"^/ws": "/",
},
selfHandleResponse: true,
onProxyRes: responseInterceptor(async (responseBuffer /*, proxyRes, req, res */) => {
return responseBuffer;
}),
});
app.use("/ws", wsProxy);
const testserver = (t.context.wsProxyServer = app.listen(wsProxyServerPort));
testserver.on("upgrade", wsProxy.upgrade);
// start the UI5 development server with multiple proxy middlewares
const { graphFromPackageDependencies } = await import("@ui5/project/graph");
const debug = false;
const graph = await graphFromPackageDependencies({
cwd: "../../showcases/ui5-app",
rootConfiguration: {
specVersion: "3.0",
metadata: {
name: "ui5-middleware-simpleproxy-test",
},
type: "application",
server: {
customMiddleware: [
{
name: "ui5-middleware-simpleproxy",
mountPath: "/local",
afterMiddleware: "compression",
configuration: {
debug,
baseUri: `http://www.example.com`,
},
},
{
name: "ui5-middleware-simpleproxy",
mountPath: "/local-ssl",
afterMiddleware: "compression",
configuration: {
debug,
baseUri: `https://secure.example.com`,
strictSSL: false,
},
},
{
name: "ui5-middleware-simpleproxy",
mountPath: "/local-subpath",
afterMiddleware: "compression",
configuration: {
debug,
baseUri: `http://www.example.com/subpath`,
},
},
{
name: "ui5-middleware-simpleproxy",
mountPath: "/proxy",
afterMiddleware: "compression",
configuration: {
debug,
baseUri: `http://www.example.com`,
httpHeaders: {
"Any-Header": "AnyHeader",
},
query: {
"any-boolean-param": true,
"any-number-param": 1337,
"any-string-param": "test#🚀",
},
excludePatterns: ["/local/**"],
removeETag: true,
username: "xxx",
password: "yyy",
skipCache: true,
},
},
{
name: "ui5-middleware-simpleproxy",
mountPath: "/corporate-proxy",
afterMiddleware: "compression",
configuration: {
debug,
baseUri: `http://corporate-proxy`,
},
},
{
name: "ui5-middleware-simpleproxy",
mountPath: "/ws",
afterMiddleware: "compression",
configuration: {
debug: true,
baseUri: `http://localhost:${wsServerPort}`,
enableWebSocket: true,
},
},
{
name: "ui5-middleware-websocket-echo",
afterMiddleware: "compression",
mountPath: "/otherws",
},
],
},
},
});
// start the UI5 server
const ui5ServerPort = await getPort();
const { serve } = await import("@ui5/server");
t.context.server = await serve(graph, {
port: ui5ServerPort,
});
const request = supertest,
wsRequest = superwstest;
t.context.request = request(`http://localhost:${ui5ServerPort}`);
t.context.wsRequest = wsRequest(`http://localhost:${ui5ServerPort}`);
t.context.wsProxyRequest = wsRequest(`http://localhost:${wsProxyServerPort}`);
});
test.after.always((t) => {
// eslint-disable-next-line jsdoc/require-jsdoc
function close(server) {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
// stop all servers
return Promise.all([close(t.context.proxyServer), close(t.context.wsProxyServer), close(t.context.wsServer), close(t.context.server)]);
});
test("HTTP basic test", async (t) => {
nock("http://www.example.com/")
.get("/debug.json")
.reply(
200,
function (/*uri, requestBody*/) {
return JSON.stringify({
headers: { ...this.req.headers },
});
},
{
"content-type": "application/json",
etag: `W/"DummyETag"`,
}
);
const { request } = t.context;
const res = await request.get("/local/debug.json");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /json/, "Correct content type");
t.is(res.headers["etag"], `W/"DummyETag"`, "ETag available");
t.not(res.body["headers"]["authorization"], "No Authorization Header");
t.not(res.body["headers"]["any-header"], "No Custom Header");
t.is(res.body["headers"]["host"], "www.example.com", "Host header changed");
});
test("HTTPS basic test", async (t) => {
nock("https://secure.example.com")
.get("/debug.json")
.reply(
200,
function (/*uri, requestBody*/) {
return JSON.stringify({
headers: { ...this.req.headers },
});
},
{
"content-type": "application/json",
etag: `W/"DummyETag"`,
}
);
const { request } = t.context;
const res = await request.get("/local-ssl/debug.json");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /json/, "Correct content type");
t.is(res.headers["etag"], `W/"DummyETag"`, "ETag available");
t.not(res.body["headers"]["authorization"], "No Authorization Header");
t.not(res.body["headers"]["any-header"], "No Custom Header");
t.is(res.body["headers"]["host"], "secure.example.com", "Host header changed");
});
test("HTTP subpath test", async (t) => {
nock("http://www.example.com")
.get("/subpath/debug.json")
.reply(
200,
function (/*uri, requestBody*/) {
return JSON.stringify({
headers: { ...this.req.headers },
});
},
{
"content-type": "application/json",
etag: `W/"DummyETag"`,
}
);
const { request } = t.context;
const res = await request.get("/local-subpath/debug.json");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /json/, "Correct content type");
t.is(res.headers["etag"], `W/"DummyETag"`, "ETag available");
t.not(res.body["headers"]["authorization"], "No Authorization Header");
t.not(res.body["headers"]["any-header"], "No Custom Header");
t.is(res.body["headers"]["host"], "www.example.com", "Host header changed");
});
test("HTTPS cookie test", async (t) => {
nock("https://secure.example.com")
.get("/cookie.txt")
.reply(
200,
function (/*uri, requestBody*/) {
return "Cookie Alert!";
},
{
"set-cookie": [
"<cookie-name>=<cookie-value>",
"<cookie-name>=<cookie-value>; Domain=<domain-value>",
"<cookie-name>=<cookie-value>; Expires=<date>",
"<cookie-name>=<cookie-value>; HttpOnly",
"<cookie-name>=<cookie-value>; Max-Age=<number>",
"<cookie-name>=<cookie-value>; Partitioned",
"<cookie-name>=<cookie-value>; Path=<path-value>",
"<cookie-name>=<cookie-value>; Secure",
"<cookie-name>=<cookie-value>; SameSite=Strict",
"<cookie-name>=<cookie-value>; SameSite=Lax",
"<cookie-name>=<cookie-value>; SameSite=None; Secure",
"<cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly",
],
"content-type": "text/plain",
}
);
const { request } = t.context;
const res = await request.get("/local-ssl/cookie.txt");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /text/, "Correct content type");
t.regex(res.text, /Cookie Alert!/, "Correct text");
t.is(
res.headers["set-cookie"].some((cookie) => {
return /(secure|domain|path|samesite)/gi.test(cookie);
}),
false,
"All cookies are unsecured!"
);
});
test("Configuration options", async (t) => {
nock("http://www.example.com")
.get("/debug.json")
.query(true)
.reply(
200,
function (/*uri, requestBody*/) {
const parsed = new URL(this.req.path, "http://www.example.com");
return JSON.stringify({
headers: { ...this.req.headers },
query: { ...querystring.parse(parsed.search.substring(1)) },
});
},
{
"content-type": "application/json",
etag: `W/"DummyETag"`,
}
);
const { request } = t.context;
const res = await request.get("/proxy/debug.json");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /json/, "Correct content type");
t.not(res.headers["etag"], `W/"DummyETag"`, "ETag available");
t.is(res.body["headers"]["authorization"], `Basic ${Buffer.from(`xxx:yyy`).toString("base64")}`, "Correct Authorization Header");
t.is(res.body["headers"]["any-header"], "AnyHeader", "Correct Custom Header");
t.is(res.body["query"]["any-boolean-param"], "true", "Correct Boolean Query Parameter");
t.is(res.body["query"]["any-number-param"], "1337", "Correct Number Query Parameter");
t.is(res.body["query"]["any-string-param"], "test#🚀", "Correct String Query Parameter");
});
test("excludePatterns option", async (t) => {
const { request } = t.context;
const res = await request.get("/proxy/local/hello.txt");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /text/, "Correct content type");
t.regex(res.text, /Hello World/, "Correct response");
});
test("skipCache option", async (t) => {
const { request } = t.context;
const res = await request.get("/proxy/local/~1234567890~b/hello.txt");
if (res.error) {
throw res.error;
}
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /text/, "Correct content type");
t.regex(res.text, /Hello World/, "Correct response");
// check to only rewrite FLP urls
const resError = await request.get("/proxy/local/~1234567890~/hello.txt");
t.is(resError.statusCode, 404, "Correct HTTP status code");
});
test("Check corporate proxy setup", async (t) => {
const { request } = t.context;
await request.get("/corporate-proxy/proxy.txt");
t.is(proxyServerHitCount, 1, "Proxy server was hit!");
await request.get("/local-ssl/proxy.txt");
t.is(proxyServerHitCount, 1, "Proxy server was not hit!");
await request.get("/corporate-proxy/proxy.txt");
t.is(proxyServerHitCount, 2, "Proxy server was hit!");
});
test("CRUD operations", async (t) => {
const { request } = t.context;
// GET
nock("http://www.example.com").get("/DataService").reply(
200,
{
key: "value",
},
{
"content-type": "application/json",
}
);
const resGET = await request.get("/local/DataService");
if (resGET.error) {
throw resGET.error;
}
t.is(resGET.statusCode, 200, "Correct HTTP status code");
t.regex(resGET.headers["content-type"], /json/, "Correct content type");
t.is(resGET.body.key, "value", "Correct content");
// POST
nock("http://www.example.com").post("/DataService").reply(
201,
{
key: "value",
},
{
"content-type": "application/json",
}
);
const resPOST = await request.post("/local/DataService");
if (resPOST.error) {
throw resPOST.error;
}
t.is(resPOST.statusCode, 201, "Correct HTTP status code");
t.regex(resPOST.headers["content-type"], /json/, "Correct content type");
t.is(resPOST.body.key, "value", "Correct content");
// PUT
nock("http://www.example.com").put("/DataService").reply(
200,
{
key: "value",
},
{
"content-type": "application/json",
}
);
const resPUT = await request.put("/local/DataService");
if (resPUT.error) {
throw resPUT.error;
}
t.is(resPUT.statusCode, 200, "Correct HTTP status code");
t.regex(resPUT.headers["content-type"], /json/, "Correct content type");
t.is(resPUT.body.key, "value", "Correct content");
// PATCH
nock("http://www.example.com").patch("/DataService").reply(204);
const resPATCH = await request.patch("/local/DataService");
if (resPATCH.error) {
throw resPATCH.error;
}
t.is(resPATCH.statusCode, 204, "Correct HTTP status code");
// DELETE
nock("http://www.example.com").delete("/DataService").reply(204);
const resDELETE = await request.delete("/local/DataService");
if (resDELETE.error) {
throw resDELETE.error;
}
t.is(resDELETE.statusCode, 204, "Correct HTTP status code");
});
test("WebSocket basic test (plain http-proxy-middleware)", async (t) => {
const { wsProxyRequest } = t.context;
await wsProxyRequest
.ws("/ws")
.expectText((msg) => {
t.is(msg, "hello");
return true;
})
.sendText("XXX")
.expectText((msg) => {
t.is(msg, "echo XXX");
return true;
})
.close();
});
test("WebSocket basic test (ui5-middleware-simpleproxy)", async (t) => {
const { wsRequest } = t.context;
await wsRequest
.ws("/ws")
.expectText((msg) => {
t.is(msg, "hello");
return true;
})
.sendText("XXX")
.expectText((msg) => {
t.is(msg, "echo XXX");
return true;
})
.close();
});
test("WebSocket basic test (ui5-middleware-websocket)", async (t) => {
const { wsRequest } = t.context;
await wsRequest
.ws("/otherws")
.wait(500)
.sendText("XXX")
.expectText((msg) => {
t.is(msg, "echo XXX");
return true;
})
.close();
});