Skip to content

Commit ae41416

Browse files
committed
Share 'no fail on server issues' logic from JA3 tests with lintcert tests
1 parent bc1c129 commit ae41416

File tree

3 files changed

+62
-32
lines changed

3 files changed

+62
-32
lines changed

test/ca.spec.ts

+25-20
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as https from 'https';
22
import * as path from 'path';
33
import * as forge from 'node-forge';
44

5-
import { expect, fetch, nodeOnly } from "./test-utils";
5+
import { expect, fetch, ignoreNetworkError, nodeOnly } from "./test-utils";
66
import * as fs from '../src/util/fs';
77

88
import { CA, generateCACertificate } from '../src/util/tls';
@@ -60,17 +60,18 @@ nodeOnly(() => {
6060
});
6161

6262
it("should be able to generate a CA certificate that passes lintcert checks", async function () {
63-
this.retries(3); // Lintcert can have intermittent connectivity blips
64-
6563
const caCertificate = await caCertificatePromise;
6664

6765
const { cert } = caCertificate;
6866

69-
const response = await fetch('https://crt.sh/lintcert', {
70-
method: 'POST',
71-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
72-
body: new URLSearchParams({'b64cert': cert})
73-
});
67+
const response = await ignoreNetworkError(
68+
fetch('https://crt.sh/lintcert', {
69+
method: 'POST',
70+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
71+
body: new URLSearchParams({'b64cert': cert})
72+
}),
73+
{ context: this }
74+
);
7475

7576
const lintOutput = await response.text();
7677

@@ -88,7 +89,6 @@ nodeOnly(() => {
8889

8990
it("should generate CA certs that can be used to create domain certs that pass lintcert checks", async function () {
9091
this.timeout(5000); // Large cert + remote request can make this slow
91-
this.retries(3); // Lintcert can have intermittent connectivity blips
9292

9393
const caCertificate = await caCertificatePromise;
9494
const ca = new CA(caCertificate.key, caCertificate.cert, 2048);
@@ -99,11 +99,14 @@ nodeOnly(() => {
9999
const certData = forge.pki.certificateFromPem(cert);
100100
expect((certData.getExtension('subjectAltName') as any).altNames[0].value).to.equal('httptoolkit.tech');
101101

102-
const response = await fetch('https://crt.sh/lintcert', {
103-
method: 'POST',
104-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
105-
body: new URLSearchParams({'b64cert': cert})
106-
});
102+
const response = await ignoreNetworkError(
103+
fetch('https://crt.sh/lintcert', {
104+
method: 'POST',
105+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
106+
body: new URLSearchParams({'b64cert': cert})
107+
}),
108+
{ context: this }
109+
);
107110

108111
expect(response.status).to.equal(200);
109112
const lintOutput = await response.text();
@@ -129,7 +132,6 @@ nodeOnly(() => {
129132

130133
it("should generate wildcard certs that pass lintcert checks for invalid subdomain names", async function () {
131134
this.timeout(5000); // Large cert + remote request can make this slow
132-
this.retries(3); // Lintcert can have intermittent connectivity blips
133135

134136
const caCertificate = await caCertificatePromise;
135137
const ca = new CA(caCertificate.key, caCertificate.cert, 2048);
@@ -139,11 +141,14 @@ nodeOnly(() => {
139141
const certData = forge.pki.certificateFromPem(cert);
140142
expect((certData.getExtension('subjectAltName') as any).altNames[0].value).to.equal('*.httptoolkit.tech');
141143

142-
const response = await fetch('https://crt.sh/lintcert', {
143-
method: 'POST',
144-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
145-
body: new URLSearchParams({'b64cert': cert})
146-
});
144+
const response = await ignoreNetworkError(
145+
fetch('https://crt.sh/lintcert', {
146+
method: 'POST',
147+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
148+
body: new URLSearchParams({'b64cert': cert})
149+
}),
150+
{ context: this }
151+
);
147152

148153
expect(response.status).to.equal(200);
149154
const lintOutput = await response.text();

test/integration/proxy.spec.ts

+4-12
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
DestroyableServer,
2424
H2_TLS_ON_TLS_SUPPORTED,
2525
OLD_TLS_SUPPORTED,
26-
delay
26+
ignoreNetworkError
2727
} from "../test-utils";
2828
import { CA } from "../../src/util/tls";
2929
import { isLocalIPv6Available } from "../../src/util/socket-util";
@@ -1039,24 +1039,16 @@ nodeOnly(() => {
10391039

10401040
await server.forAnyRequest().thenPassThrough();
10411041

1042-
let response = await Promise.race([
1042+
let response = await ignoreNetworkError( // External service, can be unreliable, c'est la vie
10431043
request.get("https://ja3er.com/json", {
10441044
headers: {
10451045
// The hash will get recorded with the user agent that's used - we don't want the database
10461046
// to fill up with records that make it clear it's Mockttp's fingerprint!
10471047
'user-agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0'
10481048
}
10491049
}),
1050-
delay(4000).then(() => { throw new Error('timeout'); })
1051-
]).catch(e => e);
1052-
1053-
if (response instanceof Error) {
1054-
// ja3er.com is often unavailable. This is annoying but hard to avoid. To handle
1055-
// this for now, we just skip the test if the server is unavailable. We only
1056-
// fail when we get a real response with a bad fingerprint.
1057-
console.warn('Skipping JA3 test due to network error:', response);
1058-
return this.skip();
1059-
}
1050+
{ context: this, timeout: 4000 }
1051+
);
10601052

10611053
const ja3Hash = JSON.parse(response).ja3_hash;
10621054

test/test-utils.ts

+33
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
FormData as FormDataPolyfill,
1414
File as FilePolyfill
1515
} from "formdata-node";
16+
import { RequestPromise } from 'request-promise-native';
1617

1718
import chai = require("chai");
1819
import chaiAsPromised = require("chai-as-promised");
@@ -87,6 +88,38 @@ export function nodeOnly(body: Function) {
8788
if (isNode) body();
8889
}
8990

91+
// Wrap a test promise that might fail due to irrelevant remote network issues, and it'll skip the test
92+
// if there's a timeout or 502 response (but still throw any other errors). This allows us to write tests
93+
// that will fail if a remote server explicitly rejects something, but make them resilient to the remote
94+
// server simply being entirely unavailable.
95+
export async function ignoreNetworkError<T extends RequestPromise | Promise<Response>>(request: T, options: {
96+
context: Mocha.Context,
97+
timeout?: number
98+
}): Promise<T> {
99+
const TimeoutError = new Error('timeout');
100+
101+
const result = await Promise.race([
102+
request.catch(e => e),
103+
delay(options.timeout ?? 1000).then(() => { throw TimeoutError })
104+
]).catch(error => {
105+
console.log(error);
106+
if (error === TimeoutError) {
107+
console.warn(`Skipping test due to network error: ${error.message || error}`);
108+
if ('abort' in request) request.abort();
109+
throw options.context.skip();
110+
} else {
111+
throw error;
112+
}
113+
});
114+
115+
if ((result as any).status === 502) {
116+
console.warn('Skipping test due to remote 502 response');
117+
throw options.context.skip();
118+
}
119+
120+
return result;
121+
}
122+
90123
const TOO_LONG_HEADER_SIZE = 1024 * (isNode ? 16 : 160) + 1;
91124
export const TOO_LONG_HEADER_VALUE = _.range(TOO_LONG_HEADER_SIZE).map(() => "X").join("");
92125

0 commit comments

Comments
 (0)