Skip to content

Commit 5710fa0

Browse files
committed
Add example.testserver.host page
1 parent 222626c commit 5710fa0

5 files changed

Lines changed: 118 additions & 8 deletions

File tree

src/endpoints/http-index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type HttpHandler = (
1616
) => MaybePromise<void>;
1717

1818
export interface HttpEndpoint {
19-
matchPath: (path: string) => boolean;
19+
matchPath: (path: string, hostnamePrefix: string | undefined) => boolean;
2020
handle: HttpHandler;
2121
}
2222

@@ -34,4 +34,5 @@ export * from './http/basic-auth.js';
3434
export * from './http/json.js';
3535
export * from './http/trailers.js';
3636
export * from './http/error/close.js';
37-
export * from './http/error/reset.js';
37+
export * from './http/error/reset.js';
38+
export * from './http/example-page.js';

src/endpoints/http/example-page.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { HttpEndpoint, HttpHandler } from '../http-index.js';
2+
3+
const matchPath = (path: string, hostnamePrefix: string | undefined) =>
4+
path === '/' && hostnamePrefix === 'example';
5+
6+
const handle: HttpHandler = (_req, res) => {
7+
// This is a static copy of the current example.com output, to allow easy migration of tests that use example.com
8+
// to use this endpoint instead. It's not intended to be perfectly identical in every possible behaviour, but
9+
// should be a good test for anything just checking basic response details.
10+
res.writeHead(200, {
11+
'content-type': 'text/html'
12+
});
13+
14+
res.end(
15+
`<!doctype html>
16+
<html>
17+
<head>
18+
<title>Example Domain</title>
19+
20+
<meta charset="utf-8" />
21+
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
22+
<meta name="viewport" content="width=device-width, initial-scale=1" />
23+
<style type="text/css">
24+
body {
25+
background-color: #f0f0f2;
26+
margin: 0;
27+
padding: 0;
28+
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
29+
30+
}
31+
div {
32+
width: 600px;
33+
margin: 5em auto;
34+
padding: 2em;
35+
background-color: #fdfdff;
36+
border-radius: 0.5em;
37+
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
38+
}
39+
a:link, a:visited {
40+
color: #38488f;
41+
text-decoration: none;
42+
}
43+
@media (max-width: 700px) {
44+
div {
45+
margin: 0 auto;
46+
width: auto;
47+
}
48+
}
49+
</style>
50+
</head>
51+
52+
<body>
53+
<div>
54+
<h1>Example Domain</h1>
55+
<p>This domain is for use in illustrative examples in documents. You may use this
56+
domain in literature without prior coordination or asking for permission.</p>
57+
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
58+
</div>
59+
</body>
60+
</html>
61+
`);
62+
}
63+
64+
export const examplePage: HttpEndpoint = {
65+
matchPath,
66+
handle
67+
};

src/http-handler.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ type RequestHandler = (
3232
) => Promise<void>;
3333

3434
function createHttpRequestHandler(options: {
35-
acmeChallengeCallback: (token: string) => string | undefined
35+
acmeChallengeCallback: (token: string) => string | undefined,
36+
rootDomain: string
3637
}): RequestHandler {
3738
return async function handleRequest(req, res) {
3839
const url = new URL(req.url!, `http://${req.headers.host}`);
@@ -57,7 +58,11 @@ function createHttpRequestHandler(options: {
5758
return;
5859
}
5960

60-
if (path === '/') {
61+
const hostnamePrefix = url.hostname.endsWith(options.rootDomain)
62+
? url.hostname.slice(0, -options.rootDomain.length - 1)
63+
: undefined;
64+
65+
if (path === '/' && !hostnamePrefix) {
6166
res.writeHead(307, {
6267
location: 'https://github.com/httptoolkit/testserver/'
6368
});
@@ -75,7 +80,7 @@ function createHttpRequestHandler(options: {
7580
}
7681

7782
const matchingEndpoint = httpEndpoints.find((endpoint) =>
78-
endpoint.matchPath(path)
83+
endpoint.matchPath(path, hostnamePrefix)
7984
);
8085

8186
if (matchingEndpoint) {
@@ -94,7 +99,8 @@ function createHttpRequestHandler(options: {
9499
}
95100

96101
export function createHttp1Handler(options: {
97-
acmeChallengeCallback: (token: string) => string | undefined
102+
acmeChallengeCallback: (token: string) => string | undefined,
103+
rootDomain: string
98104
}) {
99105
const handleRequest = createHttpRequestHandler(options);
100106
const handler = new http.Server(async (req, res) => {
@@ -124,7 +130,8 @@ export function createHttp1Handler(options: {
124130
}
125131

126132
export function createHttp2Handler(options: {
127-
acmeChallengeCallback: (token: string) => string | undefined
133+
acmeChallengeCallback: (token: string) => string | undefined,
134+
rootDomain: string
128135
}) {
129136
const handleRequest = createHttpRequestHandler(options);
130137
const handler = http2.createServer(async (req, res) => {

src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ const createTcpHandler = async (options: ServerOptions = {}) => {
9393
const tlsHandler = await createTlsHandler(tlsConfig, connProcessor);
9494

9595
const httpConfig = {
96-
acmeChallengeCallback: tlsConfig.acmeChallenge
96+
acmeChallengeCallback: tlsConfig.acmeChallenge,
97+
rootDomain: options.domain ?? 'localhost'
9798
};
9899

99100
const httpHandler = createHttp1Handler(httpConfig);

test/example-page.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as net from 'net';
2+
import { expect } from 'chai';
3+
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
4+
5+
import { createServer } from '../src/server.js';
6+
7+
describe("Example page endpoint", () => {
8+
9+
let server: DestroyableServer;
10+
let serverPort: number;
11+
12+
beforeEach(async () => {
13+
server = makeDestroyable(await createServer());
14+
await new Promise<void>((resolve) => server.listen(resolve));
15+
serverPort = (server.address() as net.AddressInfo).port;
16+
});
17+
18+
afterEach(async () => {
19+
await server.destroy();
20+
});
21+
22+
it("returns the example.com HTML page", async () => {
23+
const address = `http://example.localhost:${serverPort}/`;
24+
const response = await fetch(address);
25+
26+
expect(response.status).to.equal(200);
27+
28+
const body = await response.text();
29+
expect(body).to.include('<title>Example Domain</title>');
30+
expect(body).to.include('<h1>Example Domain</h1>');
31+
expect(body).to.include('This domain is for use in illustrative examples in documents.');
32+
});
33+
34+
});

0 commit comments

Comments
 (0)