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

Ignore Headers / Send Proxy Port / Dump Matcher Fails #604

Merged
merged 10 commits into from
May 14, 2024
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ In replay mode, this same file will be read from your tapes directory.
You can leverage this by picking a tape based on the current test's name in the `beforeEach`
block of your test suite.

## (Some) Options

`--send-proxy-port`: This flag enables the forwarding of the Proxay's local port number to the proxied host in the host header. It is particularly useful for handling redirect issues where the proxied host needs to be aware of the port on which Proxay is running to construct accurate redirect URLs. This is similar to nginx' `proxy_set_header Host $host:$server_port;`.


`--ignore-headers <headers>`: Allows users to specify a list of headers that should be ignored by Proxay's matching algorithm during request comparison. This is useful for bypassing headers that do not influence the behavior of the request but may cause mismatches, such as `x-forwarded-for` or `x-real-ip`. The headers should be provided as a comma-separated list.

Note: there is already a hardcoded list of headers that get ignored:
`accept`, `accept-encoding`, `age`, `cache-control`, `clear-site-data`, `connection`, `expires`, `from`, `host`, `postman-token`, `pragma`, `referer`, `referer-policy`, `te`, `trailer`, `transfer-encoding`, `user-agent`, `warning`, `x-datadog-trace-id`, `x-datadog-parent-id`, `traceparent`


`-r, --redact-headers <headers>`: This option enables the redaction of specific HTTP header values, which are replaced by `XXXX` to maintain privacy or confidentiality during the recording of network interactions. The headers should be provided as a comma-separated list.


`--debug-matcher-fails`: When exact request matching is enabled, this flag provides some debug information on why a request did not match any recorded tape. It is useful for troubleshooting and refining the conditions under which requests are considered equivalent, focusing on differences in headers and query parameters.


## Typical use case

Let's say you're writing tests for your client. You want your tests to run as
Expand Down
29 changes: 28 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,21 @@ 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(
"--send-proxy-port",
"Sends the proxays port to the proxied host (helps for redirect issues)",
)
.option(
"--exact-request-matching",
"Perform exact request matching instead of best-effort request matching during replay.",
)
.option(
"--debug-matcher-fails",
"In exact request matching mode, shows debug information about failed matches for headers and queries.",
)
.option(
"-r, --redact-headers <headers>",
"Request headers to redact",
"Request headers to redact (values will be replaced by XXXX)",
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks 👍

commaSeparatedList,
)
.option(
Expand All @@ -88,6 +96,11 @@ async function main(argv: string[]) {
rewriteRule,
new RewriteRules(),
)
.option(
"--ignore-headers <headers>",
"Save headers to be ignored for the matching algorithm",
commaSeparatedList,
)
.parse(argv);

const options = program.opts();
Expand All @@ -96,17 +109,22 @@ async function main(argv: string[]) {
const defaultTapeName: string = options.defaultTape;
const host: string = options.host;
const port = parseInt(options.port, 10);
const sendProxyPort: boolean =
options.sendProxyPort === undefined ? false : options.sendProxyPort;
const redactHeaders: string[] = options.redactHeaders;
const preventConditionalRequests: boolean =
!!options.dropConditionalRequestHeaders;
const httpsCA: string = options.httpsCa || "";
const httpsKey: string = options.httpsKey;
const httpsCert: string = options.httpsCert;
const rewriteBeforeDiffRules: RewriteRules = options.rewriteBeforeDiff;
const ignoreHeaders: string[] = options.ignoreHeaders;
const exactRequestMatching: boolean =
options.exactRequestMatching === undefined
? false
: options.exactRequestMatching;
const debugMatcherFails: boolean =
options.debugMatcherFails === undefined ? false : options.debugMatcherFails;

switch (initialMode) {
case "record":
Expand Down Expand Up @@ -146,10 +164,17 @@ async function main(argv: string[]) {
}
}

if (debugMatcherFails && !exactRequestMatching) {
panic(
"The --debug-matcher-fails flag can only be used with the --exact-request-matching flag.",
);
}

const server = new RecordReplayServer({
initialMode,
tapeDir,
host,
proxyPortToSend: sendProxyPort ? port : undefined,
defaultTapeName,
enableLogging: true,
redactHeaders,
Expand All @@ -158,7 +183,9 @@ async function main(argv: string[]) {
httpsKey,
httpsCert,
rewriteBeforeDiffRules,
ignoreHeaders,
exactRequestMatching,
debugMatcherFails,
});
await server.start(port);
console.log(chalk.green(`Proxying in ${initialMode} mode on port ${port}.`));
Expand Down
4 changes: 4 additions & 0 deletions src/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export function findRecordMatches(
tapeRecords: TapeRecord[],
rewriteBeforeDiffRules: RewriteRules,
exactRequestMatching: boolean,
debugMatcherFails: boolean,
ignoreHeaders: string[],
): TapeRecord[] {
let bestSimilarityScore = +Infinity;
if (exactRequestMatching) {
Expand All @@ -59,6 +61,8 @@ export function findRecordMatches(
request,
potentialMatch,
rewriteBeforeDiffRules,
ignoreHeaders,
debugMatcherFails,
);

if (similarityScore < bestSimilarityScore) {
Expand Down
5 changes: 4 additions & 1 deletion src/sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export async function send(
options: {
loggingEnabled?: boolean;
timeout?: number;
proxyPortToSend?: number;
},
): Promise<TapeRecord> {
try {
Expand All @@ -27,7 +28,9 @@ export async function send(
port,
headers: {
...request.headers,
host: hostname,
host:
hostname +
(options.proxyPortToSend ? `:${options.proxyPortToSend}` : ""),
},
timeout: options.timeout,
};
Expand Down
19 changes: 19 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class RecordReplayServer {

private mode: Mode;
private proxiedHost?: string;
private proxyPortToSend?: number;
private timeout: number;
private currentTapeRecords: TapeRecord[] = [];
private currentTape!: string;
Expand All @@ -30,13 +31,16 @@ export class RecordReplayServer {
private replayedTapes: Set<TapeRecord> = new Set();
private preventConditionalRequests?: boolean;
private rewriteBeforeDiffRules: RewriteRules;
private ignoreHeaders: string[];
private exactRequestMatching: boolean;
private debugMatcherFails: boolean;

constructor(options: {
initialMode: Mode;
tapeDir: string;
defaultTapeName: string;
host?: string;
proxyPortToSend?: number;
timeout?: number;
enableLogging?: boolean;
redactHeaders?: string[];
Expand All @@ -45,11 +49,14 @@ export class RecordReplayServer {
httpsKey?: string;
httpsCert?: string;
rewriteBeforeDiffRules?: RewriteRules;
ignoreHeaders?: string[];
exactRequestMatching?: boolean;
debugMatcherFails?: boolean;
}) {
this.currentTapeRecords = [];
this.mode = options.initialMode;
this.proxiedHost = options.host;
this.proxyPortToSend = options.proxyPortToSend;
this.timeout = options.timeout || 5000;
this.loggingEnabled = options.enableLogging || false;
const redactHeaders = options.redactHeaders || [];
Expand All @@ -58,10 +65,15 @@ export class RecordReplayServer {
this.preventConditionalRequests = options.preventConditionalRequests;
this.rewriteBeforeDiffRules =
options.rewriteBeforeDiffRules || new RewriteRules();
this.ignoreHeaders = options.ignoreHeaders || [];
this.exactRequestMatching =
options.exactRequestMatching === undefined
? false
: options.exactRequestMatching;
this.debugMatcherFails =
options.debugMatcherFails === undefined
? false
: options.debugMatcherFails;
this.loadTape(this.defaultTape);

const handler = async (
Expand Down Expand Up @@ -291,6 +303,8 @@ export class RecordReplayServer {
this.currentTapeRecords,
this.rewriteBeforeDiffRules,
this.exactRequestMatching,
this.debugMatcherFails,
this.ignoreHeaders,
),
this.replayedTapes,
);
Expand Down Expand Up @@ -331,6 +345,7 @@ export class RecordReplayServer {
{
loggingEnabled: this.loggingEnabled,
timeout: this.timeout,
proxyPortToSend: this.proxyPortToSend,
},
);
this.addRecordToTape(record);
Expand All @@ -352,6 +367,8 @@ export class RecordReplayServer {
this.currentTapeRecords,
this.rewriteBeforeDiffRules,
this.exactRequestMatching,
this.debugMatcherFails,
this.ignoreHeaders,
),
this.replayedTapes,
);
Expand All @@ -375,6 +392,7 @@ export class RecordReplayServer {
{
loggingEnabled: this.loggingEnabled,
timeout: this.timeout,
proxyPortToSend: this.proxyPortToSend,
},
);
this.addRecordToTape(record);
Expand Down Expand Up @@ -405,6 +423,7 @@ export class RecordReplayServer {
{
loggingEnabled: this.loggingEnabled,
timeout: this.timeout,
proxyPortToSend: this.proxyPortToSend,
},
);
if (this.loggingEnabled) {
Expand Down
Loading