diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index beb0831..c0567f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,6 +79,10 @@ jobs: restore-keys: | ${{ runner.os }}-npm- + - name: setup internal ssl + run: | + pipx run test/make_internal_ssl.py + - name: "Install dependencies (npm ci)" run: | npm ci diff --git a/.gitignore b/.gitignore index 9cfe6b6..95b431d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ bench/html coverage dist .nyc_output +test/ssl diff --git a/lib/configproxy.js b/lib/configproxy.js index 6e49350..c88fe9a 100644 --- a/lib/configproxy.js +++ b/lib/configproxy.js @@ -557,11 +557,11 @@ export class ConfigurableProxy extends EventEmitter { } target = new URL(target); - var proxyOptions = { target: target }; + var proxyOptions = { target }; if (that.options.clientSsl) { - proxyOptions.key = that.options.clientSsl.key; - proxyOptions.cert = that.options.clientSsl.cert; - proxyOptions.ca = that.options.clientSsl.ca; + target.key = that.options.clientSsl.key; + target.cert = that.options.clientSsl.cert; + target.ca = that.options.clientSsl.ca; } // add config argument diff --git a/lib/testutil.js b/lib/testutil.js index 008739f..fac9e68 100644 --- a/lib/testutil.js +++ b/lib/testutil.js @@ -1,14 +1,17 @@ "use strict"; import http from "node:http"; +import https from "node:https"; import { WebSocketServer } from "ws"; import { ConfigurableProxy } from "./configproxy.js"; import { defaultLogger } from "./log.js"; var servers = []; -export function addTarget(proxy, path, port, websocket, targetPath) { - var target = "http://127.0.0.1:" + port; +// TODO: make this an options dict +export function addTarget(proxy, path, port, websocket, targetPath, sslOptions) { + var proto = sslOptions ? "https" : "http"; + var target = proto + "://127.0.0.1:" + port; if (targetPath) { target = target + targetPath; } @@ -17,8 +20,12 @@ export function addTarget(proxy, path, port, websocket, targetPath) { target: target, path: path, }; + var createServer = http.createServer; + if (sslOptions) { + createServer = (cb) => https.createServer(sslOptions, cb); + } - server = http.createServer(function (req, res) { + server = createServer(function (req, res) { var reply = {}; Object.assign(reply, data); reply.url = req.url; diff --git a/test/make_internal_ssl.py b/test/make_internal_ssl.py new file mode 100644 index 0000000..522b184 --- /dev/null +++ b/test/make_internal_ssl.py @@ -0,0 +1,96 @@ +""" +Regenerate internal ssl certificates for tests +""" + +# PEP 723 dependencies +# /// script +# dependencies = [ +# "certipy", +# ] +# /// + +import asyncio +import shutil +import ssl +from pathlib import Path + +from certipy import Certipy + +ssl_dir = Path(__file__).parent.resolve() / "ssl" +port = 12345 + + +def make_certs(): + """Create certificates for proxy client and ssl backend""" + # start fresh + if ssl_dir.exists(): + shutil.rmtree(ssl_dir) + alt_names = [ + "IP:127.0.0.1", + "IP:0:0:0:0:0:0:0:1", + "DNS:localhost", + ] + certipy = Certipy(store_dir=ssl_dir) + _trust_bundles = certipy.trust_from_graph( + { + "backend-ca": ["proxy-client-ca"], + "proxy-client-ca": ["backend-ca"], + } + ) + for name in ("backend", "proxy-client"): + certipy.create_signed_pair(name, f"{name}-ca", alt_names=alt_names) + + +async def client_connected(reader, writer): + """Callback for ssl server""" + print("client connected") + msg = await reader.read(5) + print("server received", msg.decode()) + writer.write(b"pong") + + +async def ssl_backend(): + """Run a test ssl server""" + ssl_context = ssl.create_default_context( + ssl.Purpose.CLIENT_AUTH, cafile=ssl_dir / "backend-ca_trust.crt" + ) + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.load_default_certs() + ssl_context.load_cert_chain( + ssl_dir / "backend/backend.crt", ssl_dir / "backend/backend.key" + ) + await asyncio.start_server( + client_connected, host="localhost", port=port, ssl=ssl_context + ) + + +async def ssl_client(): + """Run a test ssl client""" + ssl_context = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, cafile=ssl_dir / "proxy-client-ca_trust.crt" + ) + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.load_default_certs() + ssl_context.check_hostname = True + ssl_context.load_cert_chain( + ssl_dir / "proxy-client/proxy-client.crt", + ssl_dir / "proxy-client/proxy-client.key", + ) + reader, writer = await asyncio.open_connection("localhost", port, ssl=ssl_context) + writer.write(b"ping") + msg = await reader.read(5) + print("client received", msg.decode()) + + +async def main(): + # make the certs + print(f"Making internal ssl certificates in {ssl_dir}") + make_certs() + print("Testing internal ssl setup") + await ssl_backend() + await ssl_client() + print("OK") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/test/proxy_spec.js b/test/proxy_spec.js index 3396b26..eccb046 100644 --- a/test/proxy_spec.js +++ b/test/proxy_spec.js @@ -1,3 +1,4 @@ +import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import fetch from "node-fetch"; @@ -475,4 +476,40 @@ describe("Proxy Tests", function () { done(); }); }); + + it("internal ssl test", function (done) { + if (!fs.existsSync(path.resolve(__dirname, "ssl"))) { + console.log("skipping ssl test without ssl certs. Run make_internal_ssl.py first."); + done(); + return; + } + var proxyPort = 55556; + var testPort = proxyPort + 20; + var options = { + clientSsl: { + key: fs.readFileSync(path.resolve(__dirname, "ssl/proxy-client/proxy-client.key")), + cert: fs.readFileSync(path.resolve(__dirname, "ssl/proxy-client/proxy-client.crt")), + ca: fs.readFileSync(path.resolve(__dirname, "ssl/proxy-client-ca_trust.crt")), + }, + }; + + util + .setupProxy(proxyPort, options, []) + .then((proxy) => + util.addTarget(proxy, "/backend/", testPort, false, null, { + key: fs.readFileSync(path.resolve(__dirname, "ssl/backend/backend.key")), + cert: fs.readFileSync(path.resolve(__dirname, "ssl/backend/backend.crt")), + ca: fs.readFileSync(path.resolve(__dirname, "ssl/backend-ca_trust.crt")), + requestCert: true, + }) + ) + .then(() => fetch("http://127.0.0.1:" + proxyPort + "/backend/urlpath/")) + .then((res) => { + expect(res.status).toEqual(200); + }) + .catch((err) => { + done.fail(err); + }) + .then(done); + }); });