summaryrefslogtreecommitdiffstats
path: root/test/wpt/server/server.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/wpt/server/server.mjs397
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 }