diff options
Diffstat (limited to 'test/wpt/server/server.mjs')
-rw-r--r-- | test/wpt/server/server.mjs | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs new file mode 100644 index 0000000..82b9080 --- /dev/null +++ b/test/wpt/server/server.mjs @@ -0,0 +1,397 @@ +import { once } from 'node:events' +import { createServer } from 'node:http' +import { join } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { createReadStream, readFileSync, existsSync } from 'node:fs' +import { setTimeout as sleep } from 'node:timers/promises' +import { route as networkPartitionRoute } from './routes/network-partition-key.mjs' +import { route as redirectRoute } from './routes/redirect.mjs' + +const tests = fileURLToPath(join(import.meta.url, '../../tests')) + +// https://web-platform-tests.org/tools/wptserve/docs/stash.html +class Stash extends Map { + take (key) { + if (this.has(key)) { + const value = this.get(key) + + this.delete(key) + return value.value + } + } + + put (key, value, path) { + this.set(key, { value, path }) + } +} + +const stash = new Stash() + +const server = createServer(async (req, res) => { + const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`) + + switch (fullUrl.pathname) { + case '/service-workers/cache-storage/resources/blank.html': { + res.setHeader('content-type', 'text/html') + // fall through + } + case '/service-workers/cache-storage/resources/simple.txt': + case '/fetch/content-encoding/resources/foo.octetstream.gz': + case '/fetch/content-encoding/resources/foo.text.gz': + case '/fetch/api/resources/cors-top.txt': + case '/fetch/api/resources/top.txt': + case '/mimesniff/mime-types/resources/generated-mime-types.json': + case '/mimesniff/mime-types/resources/mime-types.json': + case '/interfaces/dom.idl': + case '/interfaces/url.idl': + case '/interfaces/html.idl': + case '/interfaces/fetch.idl': + case '/interfaces/FileAPI.idl': + case '/interfaces/websockets.idl': + case '/interfaces/referrer-policy.idl': + case '/xhr/resources/utf16-bom.json': + case '/fetch/data-urls/resources/base64.json': + case '/fetch/data-urls/resources/data-urls.json': + case '/fetch/api/resources/empty.txt': + case '/fetch/api/resources/data.json': { + // If this specific resources requires custom headers + const customHeadersPath = join(tests, fullUrl.pathname + '.headers') + if (existsSync(customHeadersPath)) { + const headers = readFileSync(customHeadersPath, 'utf-8') + .trim() + .split(/\r?\n/g) + .map((h) => h.split(': ')) + + for (const [key, value] of headers) { + if (!key || !value) { + console.warn(`Skipping ${key}:${value} header pair`) + continue + } + res.setHeader(key, value) + } + } + + // https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/data.json + return createReadStream(join(tests, fullUrl.pathname)) + .on('end', () => res.end()) + .pipe(res) + } + case '/fetch/api/resources/trickle.py': { + // Note: python's time.sleep(...) takes seconds, while setTimeout + // takes ms. + const delay = parseFloat(fullUrl.searchParams.get('ms') ?? 500) + const count = parseInt(fullUrl.searchParams.get('count') ?? 50) + + // eslint-disable-next-line no-unused-vars + for await (const chunk of req); // read request body + + await sleep(delay) + + if (!fullUrl.searchParams.has('notype')) { + res.setHeader('Content-type', 'text/plain') + } + + res.statusCode = 200 + await sleep(delay) + + for (let i = 0; i < count; i++) { + res.write('TEST_TRICKLE\n') + await sleep(delay) + } + + res.end() + break + } + case '/fetch/api/resources/infinite-slow-response.py': { + // https://github.com/web-platform-tests/wpt/blob/master/fetch/api/resources/infinite-slow-response.py + const stateKey = fullUrl.searchParams.get('stateKey') ?? '' + const abortKey = fullUrl.searchParams.get('abortKey') ?? '' + + if (stateKey) { + stash.put(stateKey, 'open', fullUrl.pathname) + } + + res.setHeader('Content-Type', 'text/plain') + res.statusCode = 200 + + res.write('.'.repeat(2048)) + + while (true) { + if (!res.write('.')) { + break + } else if (abortKey && stash.take(abortKey)) { + break + } + + await sleep(100) + } + + if (stateKey) { + stash.put(stateKey, 'closed', fullUrl.pathname) + } + + res.end() + return + } + case '/fetch/api/resources/stash-take.py': { + // https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/stash-take.py + + const key = fullUrl.searchParams.get('key') + res.setHeader('Access-Control-Allow-Origin', '*') + + const took = stash.take(key, fullUrl.pathname) ?? null + + res.write(JSON.stringify(took)) + return res.end() + } + case '/fetch/api/resources/echo-content.py': { + res.setHeader('X-Request-Method', req.method) + res.setHeader('X-Request-Content-Length', req.headers['content-length'] ?? 'NO') + res.setHeader('X-Request-Content-Type', req.headers['content-type'] ?? 'NO') + res.setHeader('Content-Type', 'text/plain') + + for await (const chunk of req) { + res.write(chunk) + } + + res.end() + break + } + case '/fetch/api/resources/status.py': { + const code = parseInt(fullUrl.searchParams.get('code') ?? 200) + const text = fullUrl.searchParams.get('text') ?? 'OMG' + const content = fullUrl.searchParams.get('content') ?? '' + const type = fullUrl.searchParams.get('type') ?? '' + res.statusCode = code + res.statusMessage = text + res.setHeader('Content-Type', type) + res.setHeader('X-Request-Method', req.method) + res.end(content) + break + } + case '/fetch/api/resources/inspect-headers.py': { + const query = fullUrl.searchParams + const checkedHeaders = query.get('headers') + ?.split('|') + .map(h => h.toLowerCase()) ?? [] + + if (query.has('headers')) { + for (const header of checkedHeaders) { + if (Object.hasOwn(req.headers, header)) { + res.setHeader(`x-request-${header}`, req.headers[header] ?? '') + } + } + } + + if (query.has('cors')) { + if (Object.hasOwn(req.headers, 'origin')) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin ?? '') + } else { + res.setHeader('Access-Control-Allow-Origin', '*') + } + + res.setHeader('Access-Control-Allow-Credentials', 'true') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, HEAD') + const exposedHeaders = checkedHeaders.map(h => `x-request-${h}`).join(', ') + res.setHeader('Access-Control-Expose-Headers', exposedHeaders) + if (query.has('allow_headers')) { + res.setHeader('Access-Control-Allow-Headers', query.get('allowed_headers')) + } else { + res.setHeader('Access-Control-Allow-Headers', Object.keys(req.headers).join(', ')) + } + } + + res.setHeader('content-type', 'text/plain') + res.end('') + break + } + case '/xhr/resources/parse-headers.py': { + if (fullUrl.searchParams.has('my-custom-header')) { + const val = fullUrl.searchParams.get('my-custom-header').toLowerCase() + // res.setHeader does validation which may prevent some tests from running. + res.socket.write( + `HTTP/1.1 200 OK\r\nmy-custom-header: ${val}\r\n\r\n` + ) + } + res.end('') + break + } + case '/fetch/api/resources/bad-chunk-encoding.py': { + const query = fullUrl.searchParams + + const delay = parseFloat(query.get('ms') ?? 1000) + const count = parseInt(query.get('count') ?? 50) + await sleep(delay) + res.socket.write( + 'HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n' + ) + await sleep(delay) + + for (let i = 0; i < count; i++) { + res.socket.write('a\r\nTEST_CHUNK\r\n') + await sleep(delay) + } + + res.end('garbage') + break + } + case '/xhr/resources/headers-www-authenticate.asis': + case '/xhr/resources/headers-some-are-empty.asis': + case '/xhr/resources/headers-basic': + case '/xhr/resources/headers-double-empty.asis': + case '/xhr/resources/header-content-length-twice.asis': + case '/xhr/resources/header-content-length.asis': { + let asis = readFileSync(join(tests, fullUrl.pathname), 'utf-8') + asis = asis.replace(/\n/g, '\r\n') + asis = `${asis}\r\n` + + res.socket.write(asis) + res.end() + break + } + case '/fetch/connection-pool/resources/network-partition-key.py': { + return networkPartitionRoute(req, res, fullUrl) + } + case '/resources/top.txt': { + return createReadStream(join(tests, 'fetch/api/', fullUrl.pathname)) + .on('end', () => res.end()) + .pipe(res) + } + case '/fetch/api/resources/redirect.py': { + return redirectRoute(req, res, fullUrl) + } + case '/fetch/api/resources/method.py': { + if (fullUrl.searchParams.has('cors')) { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Credentials', 'true') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, FOO') + res.setHeader('Access-Control-Allow-Headers', 'x-test, x-foo') + res.setHeader('Access-Control-Expose-Headers', 'x-request-method') + } + + res.setHeader('x-request-method', req.method) + res.setHeader('x-request-content-type', req.headers['content-type'] ?? 'NO') + res.setHeader('x-request-content-length', req.headers['content-length'] ?? 'NO') + res.setHeader('x-request-content-encoding', req.headers['content-encoding'] ?? 'NO') + res.setHeader('x-request-content-language', req.headers['content-language'] ?? 'NO') + res.setHeader('x-request-content-location', req.headers['content-location'] ?? 'NO') + + for await (const chunk of req) { + res.write(chunk) + } + + res.end() + return + } + case '/fetch/api/resources/clean-stash.py': { + const token = fullUrl.searchParams.get('token') + const took = stash.take(token) + + if (took) { + res.end('1') + } else { + res.end('0') + } + + break + } + case '/fetch/content-encoding/resources/bad-gzip-body.py': { + res.setHeader('Content-Encoding', 'gzip') + res.end('not actually gzip') + break + } + case '/fetch/api/resources/dump-authorization-header.py': { + res.setHeader('Content-Type', 'text/html') + res.setHeader('Cache-Control', 'no-cache') + + if (req.headers.origin) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin) + res.setHeader('Access-Control-Allow-Credentials', 'true') + } else { + res.setHeader('Access-Control-Allow-Origin', '*') + } + + res.setHeader('Access-Control-Allow-Headers', 'Authorization') + res.statusCode = 200 + + if (req.headers.authorization) { + res.end(req.headers.authorization) + return + } + + res.end('none') + break + } + case '/xhr/resources/echo-headers.py': { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + + // wpt runner sends this as 1 chunk + let body = '' + + for (let i = 0; i < req.rawHeaders.length; i += 2) { + const key = req.rawHeaders[i] + const value = req.rawHeaders[i + 1] + + body += `${key}: ${value}` + } + + res.end(body) + break + } + case '/fetch/api/resources/authentication.py': { + const auth = Buffer.from(req.headers.authorization.slice('Basic '.length), 'base64') + const [user, password] = auth.toString().split(':') + + if (user === 'user' && password === 'password') { + res.end('Authentication done') + return + } + + const realm = fullUrl.searchParams.get('realm') ?? 'test' + + res.statusCode = 401 + res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`) + res.end('Please login with credentials \'user\' and \'password\'') + return + } + case '/fetch/api/resources/redirect-empty-location.py': { + res.setHeader('location', '') + res.statusCode = 302 + res.end('') + return + } + case '/service-workers/cache-storage/resources/fetch-status.py': { + const status = Number(fullUrl.searchParams.get('status')) + + res.statusCode = status + res.end() + return + } + default: { + res.statusCode = 200 + res.end(fullUrl.toString()) + } + } +}).listen(0) + +await once(server, 'listening') + +const send = (message) => { + if (typeof process.send === 'function') { + process.send(message) + } +} + +const url = `http://localhost:${server.address().port}` +console.log('server opened ' + url) +send({ server: url }) + +process.on('message', (message) => { + if (message === 'shutdown') { + server.close((err) => process.exit(err ? 1 : 0)) + } +}) + +export { server } |