-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathhandler.js
More file actions
241 lines (208 loc) · 6.43 KB
/
handler.js
File metadata and controls
241 lines (208 loc) · 6.43 KB
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import 'SHIMS';
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import sirv from 'sirv';
import { fileURLToPath } from 'node:url';
import { parse as polka_url_parser } from '@polka/url';
import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/node';
import { Server } from 'SERVER';
import { manifest, prerendered, base } from 'MANIFEST';
import { env } from 'ENV';
import { parse_as_bytes, parse_origin } from '../utils.js';
/* global ENV_PREFIX */
/* global PRECOMPRESS */
const server = new Server(manifest);
// parse_origin validates ORIGIN and throws descriptive errors for invalid values
const origin = parse_origin(env('ORIGIN', undefined));
const xff_depth = parseInt(env('XFF_DEPTH', '1'));
const address_header = env('ADDRESS_HEADER', '').toLowerCase();
const protocol_header = env('PROTOCOL_HEADER', '').toLowerCase();
const host_header = env('HOST_HEADER', '').toLowerCase();
const port_header = env('PORT_HEADER', '').toLowerCase();
const body_size_limit = parse_as_bytes(env('BODY_SIZE_LIMIT', '512K'));
if (isNaN(body_size_limit)) {
throw new Error(
`Invalid BODY_SIZE_LIMIT: '${env('BODY_SIZE_LIMIT')}'. Please provide a numeric value.`
);
}
const dir = path.dirname(fileURLToPath(import.meta.url));
const asset_dir = `${dir}/client${base}`;
await server.init({
env: /** @type {Record<string, string>} */ (process.env),
read: (file) => createReadableStream(`${asset_dir}/${file}`)
});
/**
* @param {string} path
* @param {boolean} client
*/
function serve(path, client = false) {
return fs.existsSync(path)
? sirv(path, {
etag: true,
gzip: PRECOMPRESS,
brotli: PRECOMPRESS,
setHeaders: client
? (res, pathname) => {
// only apply to build directory, not e.g. version.json
if (
pathname.startsWith(`/${manifest.appPath}/immutable/`) &&
res.statusCode === 200
) {
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
}
}
: undefined
})
: undefined;
}
// required because the static file server ignores trailing slashes
/** @returns {import('polka').Middleware} */
function serve_prerendered() {
const handler = serve(path.join(dir, 'prerendered'));
return (req, res, next) => {
let { pathname, search, query } = polka_url_parser(req);
try {
pathname = decodeURIComponent(pathname);
} catch {
// ignore invalid URI
}
if (prerendered.has(pathname)) {
return handler?.(req, res, next);
}
// remove or add trailing slash as appropriate
let location = pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/';
if (prerendered.has(location)) {
if (query) location += search;
res.writeHead(308, { location }).end();
} else {
void next();
}
};
}
/** @type {import('polka').Middleware} */
const ssr = async (req, res) => {
/** @type {Request} */
let request;
try {
request = await getRequest({
base: origin || get_origin(req.headers),
request: req,
bodySizeLimit: body_size_limit
});
} catch {
res.statusCode = 400;
res.end('Bad Request');
return;
}
await setResponse(
res,
await server.respond(request, {
platform: { req },
getClientAddress: () => {
if (address_header) {
if (!(address_header in req.headers)) {
throw new Error(
`Address header was specified with ${
ENV_PREFIX + 'ADDRESS_HEADER'
}=${address_header} but is absent from request`
);
}
const value = /** @type {string} */ (req.headers[address_header]) || '';
if (address_header === 'x-forwarded-for') {
const addresses = value.split(',');
if (xff_depth < 1) {
throw new Error(`${ENV_PREFIX + 'XFF_DEPTH'} must be a positive integer`);
}
if (xff_depth > addresses.length) {
throw new Error(
`${ENV_PREFIX + 'XFF_DEPTH'} is ${xff_depth}, but only found ${
addresses.length
} addresses`
);
}
return addresses[addresses.length - xff_depth].trim();
}
return value;
}
return (
req.connection?.remoteAddress ||
// @ts-expect-error
req.connection?.socket?.remoteAddress ||
req.socket?.remoteAddress ||
// @ts-expect-error
req.info?.remoteAddress
);
}
})
);
};
/** @param {import('polka').Middleware[]} handlers */
function sequence(handlers) {
/** @type {import('polka').Middleware} */
return (req, res, next) => {
/**
* @param {number} i
* @returns {ReturnType<import('polka').Middleware>}
*/
function handle(i) {
if (i < handlers.length) {
return handlers[i](req, res, () => handle(i + 1));
} else {
return next();
}
}
return handle(0);
};
}
/**
* @param {string} name
* @param {string | string[] | undefined} value
* @returns {string | undefined}
*/
function normalise_header(name, value) {
if (!name) return undefined;
if (Array.isArray(value)) {
if (value.length === 0) return undefined;
if (value.length === 1) return value[0];
throw new Error(
`Multiple values provided for ${name} header where only one expected: ${value}`
);
}
return value;
}
/**
* @param {import('http').IncomingHttpHeaders} headers
* @returns {string}
*/
function get_origin(headers) {
const protocol = decodeURIComponent(
normalise_header(protocol_header, headers[protocol_header]) || 'https'
);
// this helps us avoid host injections through the protocol header
if (protocol.includes(':')) {
throw new Error(
`The ${protocol_header} header specified ${protocol} which is an invalid because it includes \`:\`. It should only contain the protocol scheme (e.g. \`https\`)`
);
}
const host =
normalise_header(host_header, headers[host_header]) ||
normalise_header('host', headers['host']);
if (!host) {
const header_names = host_header ? `${host_header} or host headers` : 'host header';
throw new Error(
`Could not determine host. The request must have a value provided by the ${header_names}`
);
}
const port = normalise_header(port_header, headers[port_header]);
if (port && isNaN(+port)) {
throw new Error(
`The ${port_header} header specified ${port} which is an invalid port because it is not a number. The value should only contain the port number (e.g. 443)`
);
}
return port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
}
export const handler = sequence(
/** @type {(import('sirv').RequestHandler | import('polka').Middleware)[]} */
([serve(path.join(dir, 'client'), true), serve_prerendered(), ssr].filter(Boolean))
);