UNPKG

@galaxyproject/gx-it-proxy

Version:

A dynamic reverse proxy for Interactive Tools in Galaxy

355 lines (308 loc) 9.84 kB
import { describe, it, beforeAll, afterAll, expect } from "vitest"; import http from "http"; import url from "url"; import path from "path"; import fs from "fs"; import axios from "axios"; import axiosRetry from "axios-retry"; import { main } from "../lib/main.js"; import { DynamicProxy } from "../lib/proxy.js"; // Needed because we can’t figure out how to wait on the proxy server to get // setup (server.listening stays false forever). This greatly reduces transient // connection refused errors as a result. axiosRetry(axios, { retries: 3 }); const TEST_PORT = 9000; // Loosely based on https://stackoverflow.com/questions/16333790/node-js-quick-file-server-static-files-over-http const testServer = http.createServer(function (req, res) { console.log(`${req.method} ${req.url}`); // parse URL const parsedUrl = url.parse(req.url); // extract URL path let pathname = `.${parsedUrl.pathname}`; // based on the URL path, extract the file extension. e.g. .js, .doc, ... const ext = path.parse(pathname).ext; // maps file extension to MIME type const map = { ".ico": "image/x-icon", ".html": "text/html", ".js": "text/javascript", ".json": "application/json", ".css": "text/css", ".png": "image/png", ".jpg": "image/jpeg", ".wav": "audio/wav", ".mp3": "audio/mpeg", ".svg": "image/svg+xml", ".pdf": "application/pdf", ".doc": "application/msword", }; fs.exists(pathname, function (exist) { if (!exist) { // if the file is not found, return 404 res.statusCode = 404; res.end(`File ${pathname} not found!`); return; } // if is a directory, search for index file matching the extension if (fs.statSync(pathname).isDirectory()) { pathname += "/index" + ext; } // read file from file system fs.readFile(pathname, function (err, data) { if (err) { res.statusCode = 500; res.end(`Error getting the file: ${err}.`); } else { // if the file is found, set Content-type and send data res.setHeader("Content-type", map[ext] || "text/plain"); res.end(data); } }); }); }); const waitForServer = async function (server, listening) { for (;;) { if (server.listening === listening) { return; } // prevents app from hanging await null; } }; const useTestServer = function () { beforeAll(async () => { testServer.listen(TEST_PORT); await waitForServer(testServer, true); }); afterAll(async () => { testServer.close(); await waitForServer(testServer, false); }); }; const verifyProxyOnPort = async function (port, headers, path = "/README.md") { const res = await axios.get(`http://localhost:${port}${path}`, { headers }); const { data } = res; expect(data).toContain("# A dynamic configurable reverse proxy"); }; // -------------------------------------------------------------- // TESTS // -------------------------------------------------------------- describe("test server", () => { useTestServer(); it("should serve direct requests as files", async () => { await verifyProxyOnPort(TEST_PORT, {}); }); }); describe("DynamicProxy", () => { useTestServer(); describe("x-interactive-tool-* headers", () => { it("should respect host and port", async () => { const proxy = new DynamicProxy({ port: 5098, verbose: true }); proxy.listen(); const headers = { "x-interactive-tool-host": "localhost", "x-interactive-tool-port": TEST_PORT, }; await verifyProxyOnPort(5098, headers); proxy.close(); }); }); describe("x-interactive-tool-target headers", () => { it("should respect host and port in one header", async () => { const proxy = new DynamicProxy({ port: 5097, verbose: true }); proxy.listen(); const headers = { "x-interactive-tool-host": "localhost:" + TEST_PORT, }; await verifyProxyOnPort(5097, headers); proxy.close(); }); }); describe("map based forwarding using subdomain", () => { it("should respect session map", async () => { const sessionMap = { coolkey: { token: "cooltoken", target: { host: "localhost", port: TEST_PORT, }, }, }; const proxy = new DynamicProxy({ port: 5099, verbose: true, sessionMap, }); proxy.listen(); const headers = { host: "coolkey-cooltoken.usegalaxy.org", }; await verifyProxyOnPort(5099, headers); proxy.close(); }); }); describe("map based path forwarding to full path", () => { it("should respect session map with requires_path_in_url=true and leave path unmodified", async () => { const sessionMap = { coolkey: { token: "cooltoken", target: { host: "localhost", port: TEST_PORT, }, requires_path_in_url: true, }, }; const proxy = new DynamicProxy({ port: 5100, verbose: true, sessionMap, proxyPathPrefix: "/test_data/interactivetool/ep", }); proxy.listen(); const headers = { host: "usegalaxy.org", }; const path = "/test_data/interactivetool/ep/coolkey/cooltoken/extradir/README.md"; await verifyProxyOnPort(5100, headers, path); proxy.close(); }); }); describe("map based path forwarding to top-level path (default)", () => { it("should respect session map without requires_path_in_url and strip entry point path from url", async () => { const sessionMap = { coolkey: { token: "cooltoken", target: { host: "localhost", port: TEST_PORT, }, }, }; const proxy = new DynamicProxy({ port: 5101, verbose: true, sessionMap, proxyPathPrefix: "/interactivetool/ep", }); proxy.listen(); const headers = { host: "usegalaxy.org", }; // "/interactivetool/ep/coolkey/cooltoken" will be stripped from the path and // "/test_data/extradir/README.md" will be read and validated const path = "/interactivetool/ep/coolkey/cooltoken/test_data/extradir/README.md"; await verifyProxyOnPort(5101, headers, path); proxy.close(); }); }); describe("map based path forwarding to top-level path (requires_path_in_url=false)", () => { it("should respect session map with requires_path_in_url=false and strip entry point path from url", async () => { const sessionMap = { coolkey: { token: "cooltoken", target: { host: "localhost", port: TEST_PORT, }, requires_path_in_url: false, }, }; const proxy = new DynamicProxy({ port: 5102, verbose: true, sessionMap, proxyPathPrefix: "/interactivetool/ep", }); proxy.listen(); const headers = { host: "usegalaxy.org", }; // "/interactivetool/ep/coolkey/cooltoken" will be stripped from the path and // "/test_data/extradir/README.md" will be read and validated const path = "/interactivetool/ep/coolkey/cooltoken/test_data/extradir/README.md"; await verifyProxyOnPort(5102, headers, path); proxy.close(); }); }); describe("map based path forwarding to top-level path with entry point path in header", () => { it( 'should respect session map with requires_path_in_header_named="X-My-Header", strip entry point path from ' + 'url and instead provide it in header "X-My-Header"', async () => { const sessionMap = { coolkey: { token: "cooltoken", target: { host: "localhost", port: TEST_PORT, }, requires_path_in_header_named: "X-My-Header", }, }; const proxy = new DynamicProxy({ port: 5103, verbose: true, sessionMap, proxyPathPrefix: "/interactivetool/ep", }); proxy.listen(); const headers = { host: "usegalaxy.org" }; proxy.proxy.on("proxyReq", (proxyReq) => { expect(proxyReq.getHeader("X-My-Header")).toBe( "/interactivetool/ep/coolkey/cooltoken", ); }); const path = "/interactivetool/ep/coolkey/cooltoken/test_data/extradir/README.md"; await verifyProxyOnPort(5103, headers, path); proxy.close(); }, ); }); describe("double proxying", () => { it("should proxy across two servers", async () => { const sessionMap = { coolkey: { token: "cooltoken", target: { host: "localhost", port: TEST_PORT, }, }, }; const outerProxy = new DynamicProxy({ port: 5200, verbose: true, sessionMap, forwardIP: "localhost", forwardPort: 5201, }); const innerProxy = new DynamicProxy({ port: 5201, verbose: true }); outerProxy.listen(); innerProxy.listen(); const headers = { host: "coolkey-cooltoken.usegalaxy.org", }; await verifyProxyOnPort(5200, headers); innerProxy.close(); outerProxy.close(); }); }); }); describe("Main function", () => { useTestServer(); it("should parse simple arguments and start proxy", async () => { const proxy = main(["nodejs", "coolproxy", "--port", "5300", "--verbose"]); const headers = { "x-interactive-tool-host": "localhost", "x-interactive-tool-port": TEST_PORT, }; await verifyProxyOnPort(5300, headers); proxy.close(); }); });