forked from vercel/serve
/
server.ts
175 lines (158 loc) · 5.75 KB
/
server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// source/utilities/server.ts
// Run the server with the given configuration.
import http from 'node:http';
import https from 'node:https';
import { readFile } from 'node:fs/promises';
import handler from 'serve-handler';
import compression from 'compression';
import isPortReachable from 'is-port-reachable';
import { getNetworkAddress, registerCloseListener } from './http.js';
import { promisify } from './promise.js';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { AddressInfo } from 'node:net';
import type {
Configuration,
Options,
ParsedEndpoint,
Port,
ServerAddress,
} from '../types.js';
const compress = promisify(compression());
/**
* Starts the server and makes it listen on the given endpoint.
*
* @param endpoint - The endpoint to listen on.
* @param config - The configuration for the `serve-handler` middleware.
* @param args - The arguments passed to the CLI.
* @returns The address of the server.
*/
export const startServer = async (
endpoint: ParsedEndpoint,
config: Partial<Configuration>,
args: Partial<Options>,
previous?: Port,
): Promise<ServerAddress> => {
// Define the request handler for the server.
const serverHandler = (
request: IncomingMessage,
response: ServerResponse,
): void => {
// We can't return a promise in a HTTP request handler, so we run our code
// inside an async function instead.
const run = async () => {
type ExpressRequest = Parameters<typeof compress>[0];
type ExpressResponse = Parameters<typeof compress>[1];
if (args['--cors'])
response.setHeader('Access-Control-Allow-Origin', '*');
if (!args['--no-compression'])
await compress(request as ExpressRequest, response as ExpressResponse);
// Let the `serve-handler` module do the rest.
await handler(request, response, config);
};
// Then we run the async function, and re-throw any errors.
run().catch((error: Error) => {
throw error;
});
};
// Create the server.
const sslCert = args['--ssl-cert'];
const sslKey = args['--ssl-key'];
const sslPass = args['--ssl-pass'];
const isPFXFormat =
sslCert && /[.](?<extension>pfx|p12)$/.exec(sslCert) !== null;
const useSsl = sslCert && (sslKey || sslPass || isPFXFormat);
let serverConfig: http.ServerOptions | https.ServerOptions = {};
if (useSsl && sslCert && sslKey) {
// Format detected is PEM due to usage of SSL Key and Optional Passphrase.
serverConfig = {
key: await readFile(sslKey),
cert: await readFile(sslCert),
passphrase: sslPass ? await readFile(sslPass, 'utf8') : '',
};
} else if (useSsl && sslCert && isPFXFormat) {
// Format detected is PFX.
serverConfig = {
pfx: await readFile(sslCert),
passphrase: sslPass ? await readFile(sslPass, 'utf8') : '',
};
}
const server = useSsl
? https.createServer(serverConfig, serverHandler)
: http.createServer(serverHandler);
// Once the server starts, return the address it is running on so the CLI
// can tell the user.
const getServerDetails = () => {
// Make sure to close the server once the process ends.
registerCloseListener(() => server.close());
// Once the server has started, get the address the server is running on
// and return it.
const details = server.address() as string | AddressInfo;
let local: string | undefined;
let network: string | undefined;
if (typeof details === 'string') {
local = details;
} else if (typeof details === 'object' && details.port) {
// According to https://www.ietf.org/rfc/rfc2732.txt, IPv6 addresses
// should be surrounded by square brackets (only the address, not the
// port).
let address;
if (details.address === '::') address = 'localhost';
else if (details.family === 'IPv6') address = `[${details.address}]`;
else address = details.address;
const ip = getNetworkAddress();
const protocol = useSsl ? 'https' : 'http';
local = `${protocol}://${address}:${details.port}`;
network = ip ? `${protocol}://${ip}:${details.port}` : undefined;
}
return {
local,
network,
previous,
};
};
// Listen for any error that occurs while serving, and throw an error
// if any errors are received.
server.on('error', (error) => {
throw new Error(
`Failed to serve: ${error.stack?.toString() ?? error.message}`,
);
});
// If the endpoint is a non-zero port, make sure it is not occupied.
if (
typeof endpoint.port === 'number' &&
!isNaN(endpoint.port) &&
endpoint.port !== 0
) {
const port = endpoint.port;
const isClosed = await isPortReachable(port, {
host: endpoint.host ?? 'localhost',
});
// If the port is already taken, then start the server on a random port
// instead.
if (isClosed) return startServer({ port: 0 }, config, args, port);
// Otherwise continue on to starting the server.
}
// Finally, start the server.
return new Promise((resolve, _reject) => {
// If only a port is specified, listen on the given port on localhost.
if (
typeof endpoint.port !== 'undefined' &&
typeof endpoint.host === 'undefined'
)
server.listen(endpoint.port, () => resolve(getServerDetails()));
// If the path to a socket or a pipe is given, listen on it.
else if (
typeof endpoint.port === 'undefined' &&
typeof endpoint.host !== 'undefined'
)
server.listen(endpoint.host, () => resolve(getServerDetails()));
// If a port number and hostname are given, listen on `host:port`.
else if (
typeof endpoint.port !== 'undefined' &&
typeof endpoint.host !== 'undefined'
)
server.listen(endpoint.port, endpoint.host, () =>
resolve(getServerDetails()),
);
});
};