mockaton 10.1.0 → 10.2.0

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/index.d.ts CHANGED
@@ -97,5 +97,5 @@ export interface State {
97
97
  collectProxied: boolean
98
98
  proxyFallback: string
99
99
 
100
- corsAllowed: boolean
100
+ corsAllowed?: boolean
101
101
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "10.1.0",
5
+ "version": "10.2.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -104,7 +104,7 @@ function reinitialize(_, response) {
104
104
  async function selectCookie(req, response) {
105
105
  const error = cookie.setCurrent(await parseJSON(req))
106
106
  if (error)
107
- sendUnprocessableContent(response, error)
107
+ sendUnprocessableContent(response, error?.message || error)
108
108
  else
109
109
  sendOK(response)
110
110
  }
@@ -5,7 +5,7 @@
5
5
  <link rel="stylesheet" href="./mockaton/Dashboard.css">
6
6
  <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
- <meta name="description" content="Mock Server for developing UIs">
8
+ <meta name="description" content="HTTP Mock Server">
9
9
  <title>Mockaton</title>
10
10
  </head>
11
11
  <body>
package/src/Dashboard.js CHANGED
@@ -78,8 +78,10 @@ for (const k of Object.keys(CSS))
78
78
 
79
79
 
80
80
  /** @type {State & {
81
- * groupByMethod: boolean,
82
81
  * canProxy: boolean
82
+ * groupByMethod: boolean
83
+ * toggleGroupByMethod: () => void
84
+ * leftSideWidth?: number
83
85
  * }} */
84
86
  const state = {
85
87
  brokersByMethod: {},
@@ -87,6 +89,7 @@ const state = {
87
89
  cookies: [],
88
90
  comments: [],
89
91
  delay: 0,
92
+
90
93
  collectProxied: false,
91
94
  proxyFallback: '',
92
95
  get canProxy() {
@@ -127,19 +130,19 @@ const leftSideRef = useRef()
127
130
  function App() {
128
131
  const { leftSideWidth } = state
129
132
  return [
130
- r(Header),
131
- r(Menu),
133
+ Header(),
134
+ Menu(),
132
135
  r('main', null,
133
136
  r('div', {
134
137
  ref: leftSideRef,
135
138
  style: { width: leftSideWidth + 'px' },
136
139
  className: CSS.leftSide
137
140
  },
138
- r(MockList),
139
- r(StaticFilesList)),
141
+ MockList(),
142
+ StaticFilesList()),
140
143
  r('div', { className: CSS.rightSide },
141
- r(Resizer),
142
- r(PayloadViewer)))
144
+ Resizer(),
145
+ PayloadViewer()))
143
146
  ]
144
147
  }
145
148
 
@@ -153,15 +156,15 @@ function Header() {
153
156
  width: 160
154
157
  }),
155
158
  r('div', null,
156
- r(GlobalDelayField),
157
- r(CookieSelector),
158
- r(BulkSelector),
159
- r(ProxyFallbackField),
160
- r(ResetButton)),
159
+ GlobalDelayField(),
160
+ CookieSelector(),
161
+ BulkSelector(),
162
+ ProxyFallbackField(),
163
+ ResetButton()),
161
164
  r('button', {
162
165
  className: CSS.MenuTrigger,
163
166
  popovertarget: 'Menu'
164
- }, r(SettingsIcon))
167
+ }, SettingsIcon())
165
168
  ))
166
169
  }
167
170
 
@@ -251,7 +254,7 @@ function GlobalDelayField() {
251
254
  }
252
255
  return (
253
256
  r('label', className(CSS.Field, CSS.GlobalDelayField),
254
- r('span', null, r(TimerIcon), Strings.delay_ms),
257
+ r('span', null, TimerIcon(), Strings.delay_ms),
255
258
  r('input', {
256
259
  type: 'number',
257
260
  min: 0,
@@ -263,7 +266,7 @@ function GlobalDelayField() {
263
266
  }
264
267
 
265
268
  function ProxyFallbackField() {
266
- const { proxyFallback, collectProxied } = state
269
+ const { proxyFallback } = state
267
270
  function onChange() {
268
271
  const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
269
272
  saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
@@ -279,7 +282,7 @@ function ProxyFallbackField() {
279
282
  return (
280
283
  r('div', className(CSS.Field, CSS.FallbackBackend),
281
284
  r('label', null,
282
- r('span', null, r(CloudIcon), Strings.fallback_server),
285
+ r('span', null, CloudIcon(), Strings.fallback_server),
283
286
  r('input', {
284
287
  type: 'url',
285
288
  autocomplete: 'none',
@@ -287,14 +290,11 @@ function ProxyFallbackField() {
287
290
  value: proxyFallback,
288
291
  onChange
289
292
  })),
290
- r(SaveProxiedCheckbox, {
291
- collectProxied,
292
- disabled: !proxyFallback
293
- })))
293
+ SaveProxiedCheckbox()))
294
294
  }
295
295
 
296
- function SaveProxiedCheckbox({ disabled }) {
297
- const { collectProxied } = state
296
+ function SaveProxiedCheckbox() {
297
+ const { collectProxied, canProxy } = state
298
298
  function onChange() {
299
299
  mockaton.setCollectProxied(this.checked)
300
300
  .then(parseError)
@@ -304,7 +304,7 @@ function SaveProxiedCheckbox({ disabled }) {
304
304
  r('label', className(CSS.SaveProxiedCheckbox),
305
305
  r('input', {
306
306
  type: 'checkbox',
307
- disabled,
307
+ disabled: !canProxy,
308
308
  checked: collectProxied,
309
309
  onChange
310
310
  }),
@@ -330,8 +330,7 @@ function ResetButton() {
330
330
  /** # MockList */
331
331
 
332
332
  function MockList() {
333
- const { brokersByMethod, groupByMethod } = state
334
- const canProxy = state.canProxy
333
+ const { brokersByMethod, groupByMethod, canProxy } = state
335
334
 
336
335
  if (!Object.keys(brokersByMethod).length)
337
336
  return (
@@ -355,14 +354,14 @@ function MockList() {
355
354
  }
356
355
 
357
356
  function Row({ method, urlMask, urlMaskDittoed, broker }) {
358
- const canProxy = state.canProxy
357
+ const { canProxy } = state
359
358
  return (
360
359
  r('tr', { 'data-method': method, 'data-urlMask': urlMask },
361
- canProxy && r('td', null, r(ProxyToggler, { broker })),
362
- r('td', null, r(DelayRouteToggler, { broker })),
363
- r('td', null, r(InternalServerErrorToggler, { broker })),
364
- r('td', null, r(PreviewLink, { method, urlMask, urlMaskDittoed })),
365
- r('td', null, r(MockSelector, { broker }))))
360
+ canProxy && r('td', null, ProxyToggler(broker)),
361
+ r('td', null, DelayRouteToggler(broker)),
362
+ r('td', null, InternalServerErrorToggler(broker)),
363
+ r('td', null, PreviewLink(method, urlMask, urlMaskDittoed)),
364
+ r('td', null, MockSelector(broker))))
366
365
  }
367
366
 
368
367
  function rowsFor(targetMethod) {
@@ -385,7 +384,7 @@ function rowsFor(targetMethod) {
385
384
  }))
386
385
  }
387
386
 
388
- function PreviewLink({ method, urlMask, urlMaskDittoed }) {
387
+ function PreviewLink(method, urlMask, urlMaskDittoed) {
389
388
  async function onClick(event) {
390
389
  event.preventDefault()
391
390
  try {
@@ -408,8 +407,8 @@ function PreviewLink({ method, urlMask, urlMaskDittoed }) {
408
407
  : tail))
409
408
  }
410
409
 
411
- /** @param {{ broker: ClientMockBroker }} props */
412
- function MockSelector({ broker }) {
410
+ /** @param {ClientMockBroker} broker */
411
+ function MockSelector(broker) {
413
412
  const { groupByMethod } = state
414
413
 
415
414
  function onChange() {
@@ -457,8 +456,8 @@ function MockSelector({ broker }) {
457
456
  }, nameFor(file))))))
458
457
  }
459
458
 
460
- /** @param {{ broker: ClientMockBroker }} props */
461
- function DelayRouteToggler({ broker }) {
459
+ /** @param {ClientMockBroker} broker */
460
+ function DelayRouteToggler(broker) {
462
461
  function commit(checked) {
463
462
  const { method, urlMask } = parseFilename(broker.mocks[0])
464
463
  mockaton.setRouteIsDelayed(method, urlMask, checked)
@@ -472,8 +471,8 @@ function DelayRouteToggler({ broker }) {
472
471
  }
473
472
 
474
473
 
475
- /** @param {{ broker: ClientMockBroker }} props */
476
- function InternalServerErrorToggler({ broker }) {
474
+ /** @param {ClientMockBroker} broker */
475
+ function InternalServerErrorToggler(broker) {
477
476
  function onChange() {
478
477
  const { urlMask, method } = parseFilename(broker.mocks[0])
479
478
  mockaton.select(
@@ -499,8 +498,8 @@ function InternalServerErrorToggler({ broker }) {
499
498
  r('span', null, '500')))
500
499
  }
501
500
 
502
- /** @param {{ broker: ClientMockBroker }} props */
503
- function ProxyToggler({ broker }) {
501
+ /** @param {ClientMockBroker} broker */
502
+ function ProxyToggler(broker) {
504
503
  function onChange() {
505
504
  const { urlMask, method } = parseFilename(broker.mocks[0])
506
505
  mockaton.setRouteIsProxied(method, urlMask, this.checked)
@@ -519,7 +518,7 @@ function ProxyToggler({ broker }) {
519
518
  checked: !broker.currentMock.file,
520
519
  onChange
521
520
  }),
522
- r(CloudIcon)))
521
+ CloudIcon()))
523
522
  }
524
523
 
525
524
 
@@ -527,8 +526,7 @@ function ProxyToggler({ broker }) {
527
526
  /** # StaticFilesList */
528
527
 
529
528
  function StaticFilesList() {
530
- const { staticBrokers } = state
531
- const canProxy = state.canProxy
529
+ const { staticBrokers, canProxy } = state
532
530
  if (!Object.keys(staticBrokers).length)
533
531
  return null
534
532
  const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
@@ -543,15 +541,15 @@ function StaticFilesList() {
543
541
  r('tbody', null,
544
542
  Object.values(staticBrokers).map((broker, i) =>
545
543
  r('tr', null,
546
- canProxy && r('td', null, r(ProxyStaticToggler, {})),
547
- r('td', null, r(DelayStaticRouteToggler, { broker })),
548
- r('td', null, r(NotFoundToggler, { broker })),
544
+ canProxy && r('td', null, ProxyStaticToggler()),
545
+ r('td', null, DelayStaticRouteToggler(broker)),
546
+ r('td', null, NotFoundToggler(broker)),
549
547
  r('td', null, r('a', { href: broker.route, target: '_blank' }, dp[i]))
550
548
  )))))
551
549
  }
552
550
 
553
- /** @param {{ broker: ClientStaticBroker }} props */
554
- function DelayStaticRouteToggler({ broker }) {
551
+ /** @param {ClientStaticBroker} broker */
552
+ function DelayStaticRouteToggler(broker) {
555
553
  function commit(checked) {
556
554
  mockaton.setStaticRouteIsDelayed(broker.route, checked)
557
555
  .then(parseError)
@@ -563,8 +561,8 @@ function DelayStaticRouteToggler({ broker }) {
563
561
  })
564
562
  }
565
563
 
566
- /** @param {{ broker: ClientStaticBroker }} props */
567
- function NotFoundToggler({ broker }) {
564
+ /** @param {ClientStaticBroker} broker */
565
+ function NotFoundToggler(broker) {
568
566
  function onChange() {
569
567
  mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
570
568
  .then(parseError)
@@ -583,7 +581,7 @@ function NotFoundToggler({ broker }) {
583
581
  r('span', null, '404')))
584
582
  }
585
583
 
586
- function ProxyStaticToggler({}) { // TODO
584
+ function ProxyStaticToggler() { // TODO
587
585
  function onChange() {
588
586
  }
589
587
  return (
@@ -597,13 +595,13 @@ function ProxyStaticToggler({}) { // TODO
597
595
  disabled: true,
598
596
  onChange
599
597
  }),
600
- r(CloudIcon)))
598
+ CloudIcon()))
601
599
  }
602
600
 
603
601
 
604
602
  function ClickDragToggler({ checked, commit }) {
605
603
  function onPointerEnter(event) {
606
- if (event.buttons === 1)
604
+ if (event.buttons === 1)
607
605
  onPointerDown.call(this)
608
606
  }
609
607
  function onPointerDown() {
@@ -881,9 +879,6 @@ function className(...args) {
881
879
 
882
880
 
883
881
  function createElement(tag, props, ...children) {
884
- if (typeof tag === 'function')
885
- return tag(props)
886
-
887
882
  const node = document.createElement(tag)
888
883
  for (const [k, v] of Object.entries(props || {}))
889
884
  if (k === 'ref') v.current = node
package/src/Mockaton.js CHANGED
@@ -6,12 +6,12 @@ import { config, setup } from './config.js'
6
6
  import { dispatchMock } from './MockDispatcher.js'
7
7
  import { dispatchStatic } from './StaticDispatcher.js'
8
8
  import * as staticCollection from './staticCollection.js'
9
- import { BodyReaderError } from './utils/http-request.js'
10
9
  import * as mockBrokerCollection from './mockBrokersCollection.js'
11
10
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
12
11
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
13
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
14
- import { sendNoContent, sendInternalServerError, sendUnprocessableContent } from './utils/http-response.js'
13
+ import { BodyReaderError, isControlCharFree } from './utils/http-request.js'
14
+ import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
15
15
 
16
16
 
17
17
  export function Mockaton(options) {
@@ -39,27 +39,34 @@ export function Mockaton(options) {
39
39
 
40
40
  async function onRequest(req, response) {
41
41
  response.on('error', logger.warn)
42
+ response.setHeader('Server', 'Mockaton')
43
+
44
+ const url = req.url || ''
45
+
46
+ if (url.length > 2048) {
47
+ sendTooLongURI(response)
48
+ return
49
+ }
50
+
51
+ if (!isControlCharFree(url)) {
52
+ sendBadRequest(response)
53
+ return
54
+ }
42
55
 
43
56
  try {
44
- response.setHeader('Server', 'Mockaton')
57
+ const { method } = req
45
58
 
46
59
  if (config.corsAllowed)
47
60
  setCorsHeaders(req, response, config)
48
61
 
49
- const { url, method } = req
50
-
51
62
  if (isPreflight(req))
52
63
  sendNoContent(response)
53
-
54
64
  else if (method === 'PATCH' && apiPatchRequests.has(url))
55
65
  await apiPatchRequests.get(url)(req, response)
56
-
57
66
  else if (method === 'GET' && apiGetRequests.has(url))
58
67
  apiGetRequests.get(url)(req, response)
59
-
60
68
  else if (method === 'GET' && staticCollection.brokerByRoute(url))
61
69
  await dispatchStatic(req, response)
62
-
63
70
  else
64
71
  await dispatchMock(req, response)
65
72
  }
@@ -48,3 +48,21 @@ export function readBody(req, parser = a => a) {
48
48
  }
49
49
  })
50
50
  }
51
+
52
+ export const reControlAndDelChars = /[\x00-\x1f\x7f]/
53
+ export function isControlCharFree(url) {
54
+ try {
55
+ const decoded = decode(url)
56
+ return decoded && !reControlAndDelChars.test(decoded)
57
+ }
58
+ catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ export function decode(url) {
64
+ const candidate = decodeURIComponent(url)
65
+ return candidate === decodeURIComponent(candidate)
66
+ ? candidate
67
+ : '' // reject multiple encodings
68
+ }
@@ -28,21 +28,33 @@ export function sendNoContent(response) {
28
28
  }
29
29
 
30
30
 
31
+ export function sendBadRequest(response) {
32
+ response.statusCode = 400
33
+ logger.access(response)
34
+ response.end()
35
+ }
36
+
31
37
  export function sendNotFound(response) {
32
38
  response.statusCode = 404
33
39
  logger.access(response)
34
40
  response.end()
35
41
  }
36
42
 
43
+ export function sendTooLongURI(response) {
44
+ response.statusCode = 414
45
+ logger.access(response)
46
+ response.end()
47
+ }
48
+
37
49
  export function sendUnprocessableContent(response, error) {
38
- logger.warn(error)
50
+ logger.access(response, error)
39
51
  response.statusCode = 422
40
52
  response.end(error)
41
53
  }
42
54
 
43
55
 
44
56
  export function sendInternalServerError(response, error) {
45
- logger.error(error?.message || error, error?.stack || undefined)
57
+ logger.error(500, logger.sanitizeURL(response.req.url), error?.message || error, error?.stack || '')
46
58
  response.statusCode = 500
47
59
  response.end()
48
60
  }
@@ -1,3 +1,6 @@
1
+ import { decode, reControlAndDelChars } from './http-request.js'
2
+
3
+
1
4
  export const logger = new class {
2
5
  #level = 'normal'
3
6
 
@@ -12,16 +15,17 @@ export const logger = new class {
12
15
 
13
16
  accessMock(url, ...msg) {
14
17
  if (this.#level !== 'quiet')
15
- console.log(this.#msg('MOCK', this.#sanitizeURL(url), ...msg))
18
+ console.log(this.#msg('MOCK', this.sanitizeURL(url), ...msg))
16
19
  }
17
20
 
18
- access(response) {
21
+ access(response, error) {
19
22
  if (this.#level === 'verbose')
20
23
  console.log(this.#msg(
21
24
  'ACCESS',
22
25
  response.req.method,
23
26
  response.statusCode,
24
- this.#sanitizeURL(response.req.url)))
27
+ this.sanitizeURL(response.req.url),
28
+ error))
25
29
  }
26
30
 
27
31
  warn(...msg) {
@@ -33,10 +37,22 @@ export const logger = new class {
33
37
  }
34
38
 
35
39
  #msg(...msg) {
40
+ if (!msg.at(-1))
41
+ msg.pop()
36
42
  return [new Date().toISOString(), ...msg].join('::')
37
43
  }
38
-
39
- #sanitizeURL(url) {
40
- return decodeURIComponent(url).replace(/[\x00-\x1F\x7F\x9B]/g, '')
44
+
45
+ sanitizeURL(url) {
46
+ try {
47
+ const decoded = decode(url)
48
+ if (!decoded)
49
+ return '__MULTI_ENCODED_URL__'
50
+ return decoded
51
+ .replace(reControlAndDelChars, '')
52
+ .slice(0, 200)
53
+ }
54
+ catch {
55
+ return '__NON_DECODABLE_URL__'
56
+ }
41
57
  }
42
58
  }