Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AUTH-1236] Optionally support accepting HTTPS connections in addition to HTTP #472

Merged
merged 11 commits into from
Mar 12, 2022
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ proxay --mode passthrough --host https://api.website.com
You can also run several instances of Proxay simultaneously on different ports (for example to proxy
multiple backends). Just pick a different port (e.g. `--port 3001`).

If you want proxay to accept incoming requests on HTTPS in addition to HTTP, you can provide the HTTPS key and certificate files in PEM format on the `--https-key` and `--https-cert` arguments respectively. If the HTTPS certificate is self-signed, you probably also want to provide the CA certificate in PEM format on `--https-ca`. See the `key`, `cert`, and `ca` arguments to [`tls.createSecureContext`](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions) for what these mean.

## Specifying a tape

If you have several tests, you likely want to save recorded interactions into one tape per test,
Expand Down
18 changes: 18 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ async function main(argv: string[]) {
"--no-drop-conditional-request-headers",
"When running in record mode, by default, `If-*` headers from outgoing requests are dropped in an attempt to prevent the suite of conditional responses being returned (e.g. 304). Supplying this flag disables this default behaviour"
)
.option(
"--https-key <filename.pem>",
"Enable HTTPS server with this key. Also requires --https-cert."
)
.option(
"--https-cert <filename.pem>",
"Enable HTTPS server with this cert. Also requires --https-key."
)
.option(
"--https-ca <filename.pem>",
"Enable HTTPS server where the certificate was generated by this CA. Useful if you are using a self-signed certificate. Also requires --https-key and --https-cert."
)
.parse(argv);

const initialMode: string = (program.mode || "").toLowerCase();
Expand All @@ -37,6 +49,9 @@ async function main(argv: string[]) {
const port = parseInt(program.port, 10);
const redactHeaders: string[] = program.redactHeaders;
const preventConditionalRequests: boolean = !!program.dropConditionalRequestHeaders;
const httpsCA: string = program.httpsCa || "";
const httpsKey: string = program.httpsKey;
const httpsCert: string = program.httpsCert;

switch (initialMode) {
case "record":
Expand Down Expand Up @@ -84,6 +99,9 @@ async function main(argv: string[]) {
enableLogging: true,
redactHeaders,
preventConditionalRequests,
httpsCA,
httpsKey,
httpsCert,
});
await server.start(port);
console.log(chalk.green(`Proxying in ${initialMode} mode on port ${port}.`));
Expand Down
70 changes: 68 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import assertNever from "assert-never";
import chalk from "chalk";
import fs from "fs";
import http from "http";
import https from "https";
import net from "net";
import { ensureBuffer } from "./buffer";
import { findNextRecordToReplay, findRecordMatches } from "./matcher";
import { Mode } from "./modes";
Expand All @@ -12,7 +15,7 @@ import { TapeRecord } from "./tape";
* A server that proxies or replays requests depending on the mode.
*/
export class RecordReplayServer {
private server: http.Server;
private server: net.Server;
private persistence: Persistence;

private mode: Mode;
Expand All @@ -34,6 +37,9 @@ export class RecordReplayServer {
enableLogging?: boolean;
redactHeaders?: string[];
preventConditionalRequests?: boolean;
httpsCA?: string;
httpsKey?: string;
httpsCert?: string;
}) {
this.currentTapeRecords = [];
this.mode = options.initialMode;
Expand All @@ -46,7 +52,10 @@ export class RecordReplayServer {
this.preventConditionalRequests = options.preventConditionalRequests;
this.loadTape(this.defaultTape);

this.server = http.createServer(async (req, res) => {
const handler = async (
req: http.IncomingMessage,
res: http.ServerResponse
) => {
if (!req.url) {
if (this.loggingEnabled) {
console.error(chalk.red("Received a request without URL."));
Expand Down Expand Up @@ -97,6 +106,57 @@ export class RecordReplayServer {
res.statusCode = 500;
res.end();
}
};

const httpServer = http.createServer(handler);
let httpsServer: https.Server | null = null;
if (options.httpsKey && options.httpsCert) {
const httpsOptions = {
ca: options.httpsCA ? fs.readFileSync(options.httpsCA) : undefined,
key: fs.readFileSync(options.httpsKey),
cert: fs.readFileSync(options.httpsCert),
};
httpsServer = https.createServer(httpsOptions, handler);
}

// Copied from https://stackoverflow.com/a/42019773/16286019
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why do we have the requirement to server Https and Https over the same port?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, now that is an excellent question. The short answer is cypress. The slightly longer answer is during web testing, requests are made to proxay directly and via an nginx rewrite. The former (will shortly be) HTTPS and the later is HTTP. The later can't easily be made HTTPS due to docker-compose circular dependency reasons.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheers for the context!

I assume we can't point nginx -> proxay to a http port (using docker), and still have cypress -> proxay using the https port.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be possible, I'd have to check again. I was having issues with that earlier but I may have solved them now.

this.server = net.createServer((socket) => {
socket.once("data", (buffer) => {
// Pause the socket
socket.pause();

// Determine if this is an HTTP(s) request

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we encapsulate this https determination logic to a separate function/module? Makes it easier to understand the intent

const byte = buffer[0];

let server;
// First byte of HTTPS is a 22.
if (byte === 22) {
server = httpsServer;
} else if (32 < byte && byte < 127) {
server = httpServer;
} else {
console.error(
chalk.red(
`Unexpected starting byte of incoming request: ${byte}. Dropping request.`
)
);
}
timdawborn marked this conversation as resolved.
Show resolved Hide resolved

if (server) {
// Push the buffer back onto the front of the data stream
socket.unshift(buffer);

// Emit the socket to the HTTP(s) server
server.emit("connection", socket);
}
timdawborn marked this conversation as resolved.
Show resolved Hide resolved

// As of NodeJS 10.x the socket must be
// resumed asynchronously or the socket
// connection hangs, potentially crashing
// the process. Prior to NodeJS 10.x
// the socket may be resumed synchronously.
process.nextTick(() => socket.resume());
});
});
}

Expand Down Expand Up @@ -125,6 +185,7 @@ export class RecordReplayServer {
request.path === "/__proxay"
) {
res.end("Proxay!");
return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug fix.

}

// Sending a request to /__proxay/tape will pick a specific tape and/or a new mode.
Expand Down Expand Up @@ -166,7 +227,12 @@ export class RecordReplayServer {
this.unloadTape();
res.end(`Unloaded tape`);
}
return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a bug fix.

}

// If we got here, we don't know what to do. Return a 404.
res.statusCode = 404;
res.end(`Unhandled proxay request.\n\n${JSON.stringify(request)}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a bug fix. Currently if a __proxay/blah request is made for an unhandled route, the server hangs indefinitely. This adds a 404 response and logs out the request for debugging.

}

private async fetchResponse(request: Request): Promise<TapeRecord | null> {
Expand Down