mockaton 11.2.6 → 11.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -842
- package/index.d.ts +1 -0
- package/index.js +1 -1
- package/package.json +7 -1
- package/src/client/app-store.test.js +81 -0
- package/src/client/app.js +21 -8
- package/src/client/dom-utils.js +1 -0
- package/src/client/indexHtml.js +1 -0
- package/src/client/styles.css +6 -2
- package/src/server/Api.js +44 -47
- package/src/server/MockDispatcher.js +3 -4
- package/src/server/Mockaton.js +11 -13
- package/src/server/Mockaton.test.js +1191 -0
- package/src/server/ProxyRelay.js +3 -4
- package/src/server/StaticDispatcher.js +3 -4
- package/src/server/Watcher.js +3 -5
- package/src/server/WatcherDevClient.js +5 -7
- package/src/server/config.js +2 -4
- package/src/server/utils/{http-request.js → HttpIncomingMessage.js} +7 -1
- package/src/server/utils/HttpServerResponse.js +117 -0
- package/src/server/utils/http-cors.js +1 -1
- package/src/server/utils/http-cors.test.js +225 -0
- package/src/server/utils/jwt.js +1 -6
- package/src/server/utils/logger.js +1 -1
- package/src/server/utils/mime.js +32 -3
- package/src/server/utils/mime.test.js +24 -0
- package/src/server/utils/validate.test.js +47 -0
- package/src/server/utils/http-response.js +0 -113
package/src/server/ProxyRelay.js
CHANGED
|
@@ -3,8 +3,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
3
3
|
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
5
|
import { write, isFile } from './utils/fs.js'
|
|
6
|
-
import { readBody, BodyReaderError } from './utils/
|
|
7
|
-
import { sendUnprocessable, sendBadGateway } from './utils/http-response.js'
|
|
6
|
+
import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
|
|
8
7
|
|
|
9
8
|
import { config } from './config.js'
|
|
10
9
|
import { makeMockFilename } from './Filename.js'
|
|
@@ -23,9 +22,9 @@ export async function proxy(req, response, delay) {
|
|
|
23
22
|
}
|
|
24
23
|
catch (error) { // TESTME
|
|
25
24
|
if (error instanceof BodyReaderError)
|
|
26
|
-
|
|
25
|
+
response.unprocessable(error.name)
|
|
27
26
|
else
|
|
28
|
-
|
|
27
|
+
response.badGateway(error)
|
|
29
28
|
return
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -4,7 +4,6 @@ import { readFileSync } from 'node:fs'
|
|
|
4
4
|
import { isFile } from './utils/fs.js'
|
|
5
5
|
import { logger } from './utils/logger.js'
|
|
6
6
|
import { mimeFor } from './utils/mime.js'
|
|
7
|
-
import { sendMockNotFound, sendPartialContent } from './utils/http-response.js'
|
|
8
7
|
|
|
9
8
|
import { brokerByRoute } from './staticCollection.js'
|
|
10
9
|
import { config, calcDelay } from './config.js'
|
|
@@ -16,19 +15,19 @@ export async function dispatchStatic(req, response) {
|
|
|
16
15
|
|
|
17
16
|
setTimeout(async () => {
|
|
18
17
|
if (!broker || broker.status === 404) {
|
|
19
|
-
|
|
18
|
+
response.mockNotFound()
|
|
20
19
|
return
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
const file = join(config.staticDir, broker.route)
|
|
24
23
|
if (!isFile(file)) {
|
|
25
|
-
|
|
24
|
+
response.mockNotFound()
|
|
26
25
|
return
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
logger.accessMock(req.url, 'static200')
|
|
30
29
|
if (req.headers.range)
|
|
31
|
-
await
|
|
30
|
+
await response.partialContent(req.headers.range, file)
|
|
32
31
|
else {
|
|
33
32
|
response.setHeader('Content-Type', mimeFor(file))
|
|
34
33
|
response.end(readFileSync(file))
|
package/src/server/Watcher.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from './ApiConstants.js'
|
|
9
9
|
|
|
10
10
|
import { config } from './config.js'
|
|
11
|
-
import { sendJSON } from './utils/http-response.js'
|
|
12
11
|
import { isFile, isDirectory } from './utils/fs.js'
|
|
13
12
|
|
|
14
13
|
import * as staticCollection from './staticCollection.js'
|
|
@@ -22,7 +21,6 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
|
22
21
|
* and also renames, which are two events (delete + add).
|
|
23
22
|
*/
|
|
24
23
|
const uiSyncVersion = new class extends EventEmitter {
|
|
25
|
-
delay = Number(process.env.MOCKATON_WATCHER_DEBOUNCE_MS ?? 80)
|
|
26
24
|
version = 0
|
|
27
25
|
|
|
28
26
|
increment = /** @type {function} */ this.#debounce(() => {
|
|
@@ -41,7 +39,7 @@ const uiSyncVersion = new class extends EventEmitter {
|
|
|
41
39
|
let timer
|
|
42
40
|
return () => {
|
|
43
41
|
clearTimeout(timer)
|
|
44
|
-
timer = setTimeout(fn,
|
|
42
|
+
timer = setTimeout(fn, config.watcherDebounceMs)
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
}
|
|
@@ -101,12 +99,12 @@ export function longPollClientSyncVersion(req, response) {
|
|
|
101
99
|
const clientVersion = req.headers[HEADER_SYNC_VERSION]
|
|
102
100
|
if (clientVersion !== undefined && uiSyncVersion.version !== Number(clientVersion)) {
|
|
103
101
|
// e.g., tab was hidden while new mocks were added or removed
|
|
104
|
-
|
|
102
|
+
response.json(uiSyncVersion.version)
|
|
105
103
|
return
|
|
106
104
|
}
|
|
107
105
|
function onARR() {
|
|
108
106
|
uiSyncVersion.unsubscribe(onARR)
|
|
109
|
-
|
|
107
|
+
response.json(uiSyncVersion.version)
|
|
110
108
|
}
|
|
111
109
|
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, onARR)
|
|
112
110
|
req.on('error', () => {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { EventEmitter } from 'node:events'
|
|
3
3
|
import { watch, readdirSync } from 'node:fs'
|
|
4
|
-
import {
|
|
4
|
+
import { config } from './config.js'
|
|
5
5
|
import { LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
const DEV = process.env.NODE_ENV === 'development'
|
|
9
|
-
|
|
10
8
|
export const CLIENT_DIR = join(import.meta.dirname, '../client')
|
|
11
9
|
export const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR)
|
|
12
10
|
|
|
@@ -29,18 +27,18 @@ export function watchDevSPA() {
|
|
|
29
27
|
|
|
30
28
|
/** Realtime notify Dev UI changes */
|
|
31
29
|
export function longPollDevClientHotReload(req, response) {
|
|
32
|
-
if (!
|
|
33
|
-
|
|
30
|
+
if (!config.hotReload) {
|
|
31
|
+
response.notFound()
|
|
34
32
|
return
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
function onDevChange(file) {
|
|
38
36
|
devClientWatcher.unsubscribe(onDevChange)
|
|
39
|
-
|
|
37
|
+
response.json(file)
|
|
40
38
|
}
|
|
41
39
|
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, () => {
|
|
42
40
|
devClientWatcher.unsubscribe(onDevChange)
|
|
43
|
-
|
|
41
|
+
response.json('')
|
|
44
42
|
})
|
|
45
43
|
req.on('error', () => {
|
|
46
44
|
devClientWatcher.unsubscribe(onDevChange)
|
package/src/server/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { logger } from './utils/logger.js'
|
|
|
4
4
|
import { isDirectory } from './utils/fs.js'
|
|
5
5
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
6
6
|
import { optional, is, validate } from './utils/validate.js'
|
|
7
|
-
import { SUPPORTED_METHODS } from './utils/
|
|
7
|
+
import { SUPPORTED_METHODS } from './utils/HttpIncomingMessage.js'
|
|
8
8
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
9
9
|
|
|
10
10
|
import { jsToJsonPlugin } from './MockDispatcher.js'
|
|
@@ -21,6 +21,7 @@ const schema = {
|
|
|
21
21
|
staticDir: [resolve('mockaton-static-mocks'), optional(isDirectory)],
|
|
22
22
|
ignore: [/(\.DS_Store|~)$/, is(RegExp)],
|
|
23
23
|
watcherEnabled: [true, is(Boolean)],
|
|
24
|
+
watcherDebounceMs: [80, ms => Number.isInteger(ms) && ms >= 0],
|
|
24
25
|
|
|
25
26
|
host: ['127.0.0.1', is(String)],
|
|
26
27
|
port: [0, port => Number.isInteger(port) && port >= 0 && port < 2 ** 16], // 0 means auto-assigned
|
|
@@ -73,9 +74,6 @@ export const ConfigValidator = Object.freeze(validators)
|
|
|
73
74
|
|
|
74
75
|
/** @param {Partial<Config>} opts */
|
|
75
76
|
export function setup(opts) {
|
|
76
|
-
if (process.env.NODE_ENV !== 'development')
|
|
77
|
-
opts.hotReload = false
|
|
78
|
-
|
|
79
77
|
if (opts.mocksDir)
|
|
80
78
|
opts.mocksDir = resolve(opts.mocksDir)
|
|
81
79
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { METHODS } from 'node:http'
|
|
1
|
+
import http, { METHODS } from 'node:http'
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const SUPPORTED_METHODS = METHODS
|
|
@@ -12,6 +12,12 @@ export class BodyReaderError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export class IncomingMessage extends http.IncomingMessage {
|
|
16
|
+
json() {
|
|
17
|
+
return readBody(this, JSON.parse)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export const parseJSON = req => readBody(req, JSON.parse)
|
|
16
22
|
|
|
17
23
|
export function readBody(req, parser = a => a) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import fs, { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { logger } from './logger.js'
|
|
5
|
+
import { mimeFor } from './mime.js'
|
|
6
|
+
import { HEADER_502 } from '../ApiConstants.js'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class ServerResponse extends http.ServerResponse {
|
|
10
|
+
setHeaderList(headers) {
|
|
11
|
+
for (let i = 0; i < headers.length; i += 2)
|
|
12
|
+
this.setHeader(headers[i], headers[i + 1])
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ok() {
|
|
16
|
+
logger.access(this)
|
|
17
|
+
this.end()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html(html, csp) {
|
|
21
|
+
logger.access(this)
|
|
22
|
+
this.setHeader('Content-Type', mimeFor('html'))
|
|
23
|
+
this.setHeader('Content-Security-Policy', csp)
|
|
24
|
+
this.end(html)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
json(payload) {
|
|
28
|
+
logger.access(this)
|
|
29
|
+
this.setHeader('Content-Type', 'application/json')
|
|
30
|
+
this.end(JSON.stringify(payload))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
file(file) {
|
|
34
|
+
logger.access(this)
|
|
35
|
+
this.setHeader('Content-Type', mimeFor(file))
|
|
36
|
+
this.end(readFileSync(file, 'utf8'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
noContent() {
|
|
40
|
+
this.statusCode = 204
|
|
41
|
+
logger.access(this)
|
|
42
|
+
this.end()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
badRequest() {
|
|
47
|
+
this.statusCode = 400
|
|
48
|
+
logger.access(this)
|
|
49
|
+
this.end()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
notFound() {
|
|
53
|
+
this.statusCode = 404
|
|
54
|
+
logger.access(this)
|
|
55
|
+
this.end()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mockNotFound() {
|
|
59
|
+
this.statusCode = 404
|
|
60
|
+
logger.accessMock(this.req.url, '404')
|
|
61
|
+
this.end()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
uriTooLong() {
|
|
65
|
+
this.statusCode = 414
|
|
66
|
+
logger.access(this)
|
|
67
|
+
this.end()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
unprocessable(error) {
|
|
71
|
+
logger.access(this, error)
|
|
72
|
+
this.statusCode = 422
|
|
73
|
+
this.end(error)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
internalServerError(error) {
|
|
78
|
+
logger.error(500, this.req.url, error?.message || error, error?.stack || '')
|
|
79
|
+
this.statusCode = 500
|
|
80
|
+
this.end()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
badGateway(error) {
|
|
84
|
+
logger.warn('Fallback Proxy Error:', error.cause.message)
|
|
85
|
+
this.statusCode = 502
|
|
86
|
+
this.setHeader(HEADER_502, 1)
|
|
87
|
+
this.end()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async partialContent(range, file) {
|
|
92
|
+
const { size } = await fs.promises.lstat(file)
|
|
93
|
+
let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
|
|
94
|
+
if (isNaN(end)) end = size - 1
|
|
95
|
+
if (isNaN(start)) start = size - end
|
|
96
|
+
|
|
97
|
+
if (start < 0 || start > end || start >= size || end >= size) {
|
|
98
|
+
this.statusCode = 416 // Range Not Satisfiable
|
|
99
|
+
this.setHeader('Content-Range', `bytes */${size}`)
|
|
100
|
+
this.end()
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.statusCode = 206 // Partial Content
|
|
104
|
+
this.setHeader('Accept-Ranges', 'bytes')
|
|
105
|
+
this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
106
|
+
this.setHeader('Content-Type', mimeFor(file))
|
|
107
|
+
const reader = fs.createReadStream(file, { start, end })
|
|
108
|
+
const response = this
|
|
109
|
+
reader.on('open', function () {
|
|
110
|
+
this.pipe(response)
|
|
111
|
+
})
|
|
112
|
+
reader.on('error', error => {
|
|
113
|
+
this.internalServerError(error)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { equal } from 'node:assert/strict'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
import { describe, test, after } from 'node:test'
|
|
5
|
+
import { isPreflight, setCorsHeaders, CorsHeader as CH } from './http-cors.js'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function headerIs(response, header, value) {
|
|
9
|
+
equal(response.headers.get(header), value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FooDotTest = 'https://foo.test'
|
|
13
|
+
const AllowedDotTest = 'https://allowed.test'
|
|
14
|
+
const NotAllowedDotTest = 'https://not-allowed.test'
|
|
15
|
+
|
|
16
|
+
await describe('CORS', async () => {
|
|
17
|
+
let corsConfig = {}
|
|
18
|
+
|
|
19
|
+
const server = createServer((req, response) => {
|
|
20
|
+
setCorsHeaders(req, response, corsConfig)
|
|
21
|
+
if (isPreflight(req)) {
|
|
22
|
+
response.statusCode = 204
|
|
23
|
+
response.end()
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
response.end('NON_PREFLIGHT')
|
|
27
|
+
})
|
|
28
|
+
await promisify(server.listen).bind(server, 0, '127.0.0.1')()
|
|
29
|
+
after(() => {
|
|
30
|
+
server.close()
|
|
31
|
+
})
|
|
32
|
+
function preflight(headers, method = 'OPTIONS') {
|
|
33
|
+
const { address, port } = server.address()
|
|
34
|
+
return fetch(`http://${address}:${port}/`, { method, headers })
|
|
35
|
+
}
|
|
36
|
+
function request(headers, method) {
|
|
37
|
+
const { address, port } = server.address()
|
|
38
|
+
return fetch(`http://${address}:${port}/`, { method, headers })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await describe('Identifies Preflight Requests', async () => {
|
|
42
|
+
const requiredRequestHeaders = {
|
|
43
|
+
[CH.Origin]: 'http://localhost:9999',
|
|
44
|
+
[CH.AcRequestMethod]: 'POST'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await test('Ignores non-OPTIONS requests', async () => {
|
|
48
|
+
const res = await request(requiredRequestHeaders, 'POST')
|
|
49
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await test(`Ignores non-parseable req ${CH.Origin} header`, async () => {
|
|
53
|
+
const headers = {
|
|
54
|
+
...requiredRequestHeaders,
|
|
55
|
+
[CH.Origin]: 'non-url'
|
|
56
|
+
}
|
|
57
|
+
const res = await preflight(headers)
|
|
58
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await test(`Ignores missing method in ${CH.AcRequestMethod} header`, async () => {
|
|
62
|
+
const headers = { ...requiredRequestHeaders }
|
|
63
|
+
delete headers[CH.AcRequestMethod]
|
|
64
|
+
const res = await preflight(headers)
|
|
65
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await test(`Ignores non-standard method in ${CH.AcRequestMethod} header`, async () => {
|
|
69
|
+
const headers = {
|
|
70
|
+
...requiredRequestHeaders,
|
|
71
|
+
[CH.AcRequestMethod]: 'NON_STANDARD'
|
|
72
|
+
}
|
|
73
|
+
const res = await preflight(headers)
|
|
74
|
+
equal(await res.text(), 'NON_PREFLIGHT')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await test('204 valid preflights', async () => {
|
|
78
|
+
const res = await preflight(requiredRequestHeaders)
|
|
79
|
+
equal(res.status, 204)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await describe('Preflight Response Headers', async () => {
|
|
84
|
+
await test('no origins allowed', async () => {
|
|
85
|
+
corsConfig = {
|
|
86
|
+
corsOrigins: [],
|
|
87
|
+
corsMethods: ['GET']
|
|
88
|
+
}
|
|
89
|
+
const p = await preflight({
|
|
90
|
+
[CH.Origin]: FooDotTest,
|
|
91
|
+
[CH.AcRequestMethod]: 'GET'
|
|
92
|
+
})
|
|
93
|
+
headerIs(p, CH.AcAllowOrigin, null)
|
|
94
|
+
headerIs(p, CH.AcAllowMethods, null)
|
|
95
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
96
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
97
|
+
headerIs(p, CH.AcMaxAge, null)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await test('not in allowed origins', async () => {
|
|
101
|
+
corsConfig = {
|
|
102
|
+
corsOrigins: [AllowedDotTest],
|
|
103
|
+
corsMethods: ['GET']
|
|
104
|
+
}
|
|
105
|
+
const p = await preflight({
|
|
106
|
+
[CH.Origin]: NotAllowedDotTest,
|
|
107
|
+
[CH.AcRequestMethod]: 'GET'
|
|
108
|
+
})
|
|
109
|
+
headerIs(p, CH.AcAllowOrigin, null)
|
|
110
|
+
headerIs(p, CH.AcAllowMethods, null)
|
|
111
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
112
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await test('origin and method match', async () => {
|
|
116
|
+
corsConfig = {
|
|
117
|
+
corsOrigins: [AllowedDotTest],
|
|
118
|
+
corsMethods: ['GET']
|
|
119
|
+
}
|
|
120
|
+
const p = await preflight({
|
|
121
|
+
[CH.Origin]: AllowedDotTest,
|
|
122
|
+
[CH.AcRequestMethod]: 'GET'
|
|
123
|
+
})
|
|
124
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotTest)
|
|
125
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
126
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
127
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await test('origin matches from multiple', async () => {
|
|
131
|
+
corsConfig = {
|
|
132
|
+
corsOrigins: [AllowedDotTest, FooDotTest],
|
|
133
|
+
corsMethods: ['GET']
|
|
134
|
+
}
|
|
135
|
+
const p = await preflight({
|
|
136
|
+
[CH.Origin]: AllowedDotTest,
|
|
137
|
+
[CH.AcRequestMethod]: 'GET'
|
|
138
|
+
})
|
|
139
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotTest)
|
|
140
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
141
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
142
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await test('wildcard origin', async () => {
|
|
146
|
+
corsConfig = {
|
|
147
|
+
corsOrigins: ['*'],
|
|
148
|
+
corsMethods: ['GET']
|
|
149
|
+
}
|
|
150
|
+
const p = await preflight({
|
|
151
|
+
[CH.Origin]: FooDotTest,
|
|
152
|
+
[CH.AcRequestMethod]: 'GET'
|
|
153
|
+
})
|
|
154
|
+
headerIs(p, CH.AcAllowOrigin, FooDotTest)
|
|
155
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
156
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
157
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await test(`wildcard and credentials`, async () => {
|
|
161
|
+
corsConfig = {
|
|
162
|
+
corsOrigins: ['*'],
|
|
163
|
+
corsMethods: ['GET'],
|
|
164
|
+
corsCredentials: true
|
|
165
|
+
}
|
|
166
|
+
const p = await preflight({
|
|
167
|
+
[CH.Origin]: FooDotTest,
|
|
168
|
+
[CH.AcRequestMethod]: 'GET'
|
|
169
|
+
})
|
|
170
|
+
headerIs(p, CH.AcAllowOrigin, FooDotTest)
|
|
171
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
172
|
+
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
173
|
+
headerIs(p, CH.AcAllowHeaders, null)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
await test(`wildcard, credentials, and headers`, async () => {
|
|
177
|
+
corsConfig = {
|
|
178
|
+
corsOrigins: ['*'],
|
|
179
|
+
corsMethods: ['GET'],
|
|
180
|
+
corsCredentials: true,
|
|
181
|
+
corsHeaders: ['content-type', 'my-header']
|
|
182
|
+
}
|
|
183
|
+
const p = await preflight({
|
|
184
|
+
[CH.Origin]: FooDotTest,
|
|
185
|
+
[CH.AcRequestMethod]: 'GET'
|
|
186
|
+
})
|
|
187
|
+
headerIs(p, CH.AcAllowOrigin, FooDotTest)
|
|
188
|
+
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
189
|
+
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
190
|
+
headerIs(p, CH.AcAllowHeaders, 'content-type,my-header')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await describe('Non-Preflight (Actual Response) Headers', async () => {
|
|
195
|
+
await test('no origins allowed', async () => {
|
|
196
|
+
corsConfig = {
|
|
197
|
+
corsOrigins: [],
|
|
198
|
+
corsMethods: ['GET']
|
|
199
|
+
}
|
|
200
|
+
const p = await request({
|
|
201
|
+
[CH.Origin]: NotAllowedDotTest
|
|
202
|
+
})
|
|
203
|
+
equal(p.status, 200)
|
|
204
|
+
headerIs(p, CH.AcAllowOrigin, null)
|
|
205
|
+
headerIs(p, CH.AcAllowCredentials, null)
|
|
206
|
+
headerIs(p, CH.AcExposeHeaders, null)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await test('origin allowed', async () => {
|
|
210
|
+
corsConfig = {
|
|
211
|
+
corsOrigins: [AllowedDotTest],
|
|
212
|
+
corsMethods: ['GET'],
|
|
213
|
+
corsCredentials: true,
|
|
214
|
+
corsExposedHeaders: ['x-h1', 'x-h2']
|
|
215
|
+
}
|
|
216
|
+
const p = await request({
|
|
217
|
+
[CH.Origin]: AllowedDotTest
|
|
218
|
+
})
|
|
219
|
+
equal(p.status, 200)
|
|
220
|
+
headerIs(p, CH.AcAllowOrigin, AllowedDotTest)
|
|
221
|
+
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
222
|
+
headerIs(p, CH.AcExposeHeaders, 'x-h1,x-h2')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|
package/src/server/utils/jwt.js
CHANGED
|
@@ -9,13 +9,8 @@ export function jwtCookie(cookieName, payload, path = '/') {
|
|
|
9
9
|
function jwt(payload) {
|
|
10
10
|
return [
|
|
11
11
|
'Header_Not_In_Use',
|
|
12
|
-
|
|
12
|
+
Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url'),
|
|
13
13
|
'Signature_Not_In_Use'
|
|
14
14
|
].join('.')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function toBase64Url(obj) {
|
|
18
|
-
return btoa(JSON.stringify(obj))
|
|
19
|
-
.replace('+', '-')
|
|
20
|
-
.replace('/', '_')
|
|
21
|
-
}
|
package/src/server/utils/mime.js
CHANGED
|
@@ -11,9 +11,13 @@ import { UNKNOWN_MIME_EXT } from '../ApiConstants.js'
|
|
|
11
11
|
const extToMime = {
|
|
12
12
|
'3g2': 'video/3gpp2',
|
|
13
13
|
'3gp': 'video/3gpp',
|
|
14
|
+
'3mf': 'model/3mf',
|
|
14
15
|
'7z': 'application/x-7z-compressed',
|
|
15
16
|
aac: 'audio/aac',
|
|
16
17
|
abw: 'application/x-abiword',
|
|
18
|
+
aif: 'audio/aiff',
|
|
19
|
+
aifc: 'audio/aiff',
|
|
20
|
+
aiff: 'audio/aiff',
|
|
17
21
|
apng: 'image/apng',
|
|
18
22
|
arc: 'application/x-freearc',
|
|
19
23
|
avi: 'video/x-msvideo',
|
|
@@ -28,12 +32,22 @@ const extToMime = {
|
|
|
28
32
|
csh: 'application/x-csh',
|
|
29
33
|
css: 'text/css',
|
|
30
34
|
csv: 'text/csv',
|
|
35
|
+
dae: 'model/vnd.collada+xml',
|
|
31
36
|
doc: 'application/msword',
|
|
32
37
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
38
|
+
drc: 'model/vnd.draco',
|
|
39
|
+
eml: 'message/rfc822',
|
|
33
40
|
eot: 'application/vnd.ms-fontobject',
|
|
34
41
|
epub: 'application/epub+zip',
|
|
42
|
+
exe: 'application/vnd.microsoft.portable-executable',
|
|
43
|
+
fbx: 'application/octet-stream',
|
|
44
|
+
flac: 'audio/flac',
|
|
35
45
|
gif: 'image/gif',
|
|
46
|
+
glb: 'model/gltf-binary',
|
|
47
|
+
gltf: 'model/gltf+json',
|
|
36
48
|
gz: 'application/gzip',
|
|
49
|
+
heic: 'image/heic',
|
|
50
|
+
heif: 'image/heif',
|
|
37
51
|
htm: 'text/html',
|
|
38
52
|
html: 'text/html',
|
|
39
53
|
ico: 'image/vnd.microsoft.icon',
|
|
@@ -44,13 +58,21 @@ const extToMime = {
|
|
|
44
58
|
js: 'application/javascript',
|
|
45
59
|
json: 'application/json',
|
|
46
60
|
jsonld: 'application/ld+json',
|
|
61
|
+
lz: 'application/x-lzip',
|
|
62
|
+
m4a: 'audio/mp4',
|
|
63
|
+
map: 'application/json',
|
|
64
|
+
md: 'text/markdown',
|
|
47
65
|
mid: 'audio/midi',
|
|
48
66
|
midi: 'audio/midi',
|
|
49
67
|
mjs: 'text/javascript',
|
|
68
|
+
mkv: 'video/x-matroska',
|
|
69
|
+
mov: 'video/quicktime',
|
|
50
70
|
mp3: 'audio/mpeg',
|
|
51
71
|
mp4: 'video/mp4',
|
|
52
72
|
mpeg: 'video/mpeg',
|
|
53
73
|
mpkg: 'application/vnd.apple.installer+xml',
|
|
74
|
+
mtl: 'text/plain',
|
|
75
|
+
obj: 'text/plain',
|
|
54
76
|
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
55
77
|
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
56
78
|
odt: 'application/vnd.oasis.opendocument.text',
|
|
@@ -61,19 +83,24 @@ const extToMime = {
|
|
|
61
83
|
otf: 'font/otf',
|
|
62
84
|
pdf: 'application/pdf',
|
|
63
85
|
php: 'application/x-httpd-php',
|
|
86
|
+
ply: 'application/octet-stream',
|
|
64
87
|
png: 'image/png',
|
|
65
88
|
ppt: 'application/vnd.ms-powerpoint',
|
|
66
89
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
67
90
|
rar: 'application/vnd.rar',
|
|
68
91
|
rtf: 'application/rtf',
|
|
69
92
|
sh: 'application/x-sh',
|
|
93
|
+
stl: 'model/stl',
|
|
70
94
|
svg: 'image/svg+xml',
|
|
71
95
|
tar: 'application/x-tar',
|
|
72
96
|
tif: 'image/tiff',
|
|
73
97
|
ts: 'video/mp2t',
|
|
74
98
|
ttf: 'font/ttf',
|
|
75
99
|
txt: 'text/plain',
|
|
100
|
+
usd: 'model/vnd.usd',
|
|
101
|
+
usdz: 'model/vnd.usdz+zip',
|
|
76
102
|
vsd: 'application/vnd.visio',
|
|
103
|
+
wasm: 'application/wasm',
|
|
77
104
|
wav: 'audio/wav',
|
|
78
105
|
weba: 'audio/webm',
|
|
79
106
|
webm: 'video/webm',
|
|
@@ -85,9 +112,11 @@ const extToMime = {
|
|
|
85
112
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
86
113
|
xml: 'application/xml',
|
|
87
114
|
xul: 'application/vnd.mozilla.xul+xml',
|
|
115
|
+
xz: 'application/x-xz',
|
|
88
116
|
yaml: 'application/yaml',
|
|
89
117
|
yml: 'application/yaml',
|
|
90
|
-
zip: 'application/zip'
|
|
118
|
+
zip: 'application/zip',
|
|
119
|
+
zst: 'application/zstd'
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
const mimeToExt = mapMimeToExt(extToMime)
|
|
@@ -105,8 +134,8 @@ export function mimeFor(filename) {
|
|
|
105
134
|
}
|
|
106
135
|
function extname(filename) {
|
|
107
136
|
const i = filename.lastIndexOf('.')
|
|
108
|
-
return i === -1
|
|
109
|
-
? ''
|
|
137
|
+
return i === -1
|
|
138
|
+
? ''
|
|
110
139
|
: filename.slice(i + 1)
|
|
111
140
|
}
|
|
112
141
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { equal } from 'node:assert/strict'
|
|
3
|
+
import { parseMime, extFor, mimeFor } from './mime.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('parseMime', () => [
|
|
7
|
+
'text/html',
|
|
8
|
+
'TEXT/html',
|
|
9
|
+
'text/html; charset=utf-8'
|
|
10
|
+
].map(input =>
|
|
11
|
+
equal(parseMime(input), 'text/html')))
|
|
12
|
+
|
|
13
|
+
test('extFor', () => [
|
|
14
|
+
'text/html',
|
|
15
|
+
'Text/html',
|
|
16
|
+
'text/Html; charset=UTF-16'
|
|
17
|
+
].map(input =>
|
|
18
|
+
equal(extFor(input), 'html')))
|
|
19
|
+
|
|
20
|
+
test('mimeFor', () => [
|
|
21
|
+
'file.html',
|
|
22
|
+
'file.HTmL'
|
|
23
|
+
].map(input =>
|
|
24
|
+
equal(mimeFor(input), 'text/html')))
|