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
12 changes: 12 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ async function main(argv: string[]) {
.option("--default-tape <tape-name>", "Name of the default tape", "default")
.option("-h, --host <host>", "Host to proxy (not required in replay mode)")
.option("-p, --port <port>", "Local port to serve on", "3000")
.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(
"-r, --redact-headers <headers>",
"Request headers to redact",
Expand All @@ -32,6 +40,8 @@ async function main(argv: string[]) {
const host: string = program.host;
const port = parseInt(program.port, 10);
const redactHeaders: string[] = program.redactHeaders;
const httpsKey: string = program.httpsKey;
const httpsCert: string = program.httpsCert;

switch (initialMode) {
case "record":
Expand Down Expand Up @@ -72,6 +82,8 @@ async function main(argv: string[]) {
defaultTapeName,
enableLogging: true,
redactHeaders,
httpsKey,
httpsCert,
});
await server.start(port);
console.log(chalk.green(`Proxying in ${initialMode} mode on port ${port}.`));
Expand Down
56 changes: 54 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 @@ -32,6 +35,8 @@ export class RecordReplayServer {
timeout?: number;
enableLogging?: boolean;
redactHeaders?: string[];
httpsKey?: string;
httpsCert?: string;
}) {
this.currentTapeRecords = [];
this.mode = options.initialMode;
Expand All @@ -43,7 +48,10 @@ export class RecordReplayServer {
this.defaultTape = options.defaultTapeName;
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 @@ -85,6 +93,50 @@ 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 = {
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;
}
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