]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(simulator): reject supervision URLs that break once the station identity is appen...
authorVitor Coelho <6161176+VitorCoelho@users.noreply.github.com>
Fri, 30 Oct 2026 14:59:19 +0000 (14:59 +0000)
committerGitHub <noreply@github.com>
Fri, 30 Oct 2026 14:59:19 +0000 (14:59 +0000)
* fix(simulator): reject supervision URLs that break once the station identity is appended

The charging station dials the supervision URL with its identity appended.
After a trailing '#' the identity becomes a fragment, which the ws client
rejects, and a ws+unix URL without a ':' between the socket path and the
request path gets the identity appended to the socket path, so the
connection fails (ENOTDIR). Reject both in SupervisionUrlSchema.

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* test(utils): check the supervision URL schema against the URL the station dials

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* docs(readme): document the ws+unix supervision URL form

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* fix(simulator): reject supervision URLs with a query string or an ambiguous ws+unix separator

The charging station identity is appended to the supervision URL, so after
a '?' it lands in the query (ws://h/ocpp?a=b dials /ocpp with query
'a=b/CS-1'). ws keeps only the first two ':'-separated parts of a ws+unix
pathname, so 'ws+unix:///s.sock:/oc:pp' dials /oc, and 'ws+unix:///:/ocpp'
dials the socket '/'. Reject these, checked against real TCP and Unix-socket
servers.

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* test(utils): cover query strings and extra ws+unix separators in supervision URLs

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* docs(readme): document the query string and ws+unix separator rules for supervision URLs

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* fix(simulator): accept one-character ws+unix socket paths and require a '/' request path

A relative socket path such as 's' in 'ws+unix:s:/OCPP16' is valid, so
require a socket path other than '/' instead of two characters. A request
path without a leading '/' (ws+unix:s:OCPP16) makes the server answer 400,
so require it to be empty or to start with '/'.

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
* test(utils): cover one-character ws+unix socket paths and request paths without '/'

Regressions for the SupervisionUrlSchema fix: 'ws+unix:s:/OCPP16' and
'ws+unix:./s:/OCPP16' are accepted, 'ws+unix:s:OCPP16' and
'ws+unix:///tmp/simulator.sock:OCPP16' are rejected.

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
---------

Signed-off-by: VitorCoelho <vitorc195@gmail.com>
Co-authored-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
README.md
src/utils/ConfigurationSchema.ts
tests/utils/ConfigurationSchema.test.ts

index a14a6820cd4809c228a857b839abd2fe90c14edf..0b228549ad2db4f3e48804204fb19788f68e127f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -180,7 +180,7 @@ The `<assets-dir>/configurations` subdirectory is created by the runtime when pe
 | Key                        | Value(s)                                     | Default Value                                                                                                                                                                                                                                                                                                                        | Value type                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
 | -------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
 | $schemaVersion             | 2                                            | 2                                                                                                                                                                                                                                                                                                                                    | integer                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | Configuration schema version. Set to 2. Files without this field are migrated from v0; deprecated keys are remapped on every load.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
-| supervisionUrls            |                                              | []                                                                                                                                                                                                                                                                                                                                   | string \| string[]                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | string or strings array containing global connection URIs to OCPP-J servers (`ws://`, `wss://`, `http://`, `https://` or `ws+unix://`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
+| supervisionUrls            |                                              | []                                                                                                                                                                                                                                                                                                                                   | string \| string[]                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | string or strings array containing global connection URIs to OCPP-J servers (`ws://`, `wss://`, `http://`, `https://` or `ws+unix://`; no `#` fragment or `?` query, since the charging station identity is appended to the URL; a `ws+unix://` URL separates the socket path from the request path with one `:`, e.g. `ws+unix:///path/to/socket.sock:/OCPP16`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
 | supervisionUrlDistribution | round-robin/random/charging-station-affinity | charging-station-affinity                                                                                                                                                                                                                                                                                                            | string                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | supervision urls distribution policy to simulated charging stations                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
 | log                        |                                              | {<br />"enabled": true,<br />"file": "logs/combined.log",<br />"errorFile": "logs/error.log",<br />"statisticsInterval": 60,<br />"level": "info",<br />"console": false,<br />"format": "simple",<br />"rotate": true<br />}                                                                                                        | {<br />enabled?: boolean;<br />file?: string;<br />errorFile?: string;<br />statisticsInterval?: number;<br />level?: string;<br />console?: boolean;<br />format?: string;<br />rotate?: boolean;<br />maxFiles?: string \| number;<br />maxSize?: string \| number;<br />}                                                                                                                                                                                                                                                                                                                                                                            | Log configuration section:<br />- _enabled_: enable logging<br />- _file_: log file relative path<br />- _errorFile_: error log file relative path<br />- _statisticsInterval_: seconds between charging stations statistics output in the logs<br />- _level_: emerg/alert/crit/error/warning/notice/info/debug [winston](https://github.com/winstonjs/winston) logging level</br >- _console_: output logs on the console<br />- _format_: [winston](https://github.com/winstonjs/winston) log format<br />- _rotate_: enable daily log files rotation<br />- _maxFiles_: maximum number of log files: https://github.com/winstonjs/winston-daily-rotate-file#options<br />- _maxSize_: maximum size of log files in bytes, or units of kb, mb, and gb: https://github.com/winstonjs/winston-daily-rotate-file#options                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
 | worker                     |                                              | {<br />"processType": "workerSet",<br />"startDelay": 500,<br />"elementAddDelay": 0,<br />"elementsPerWorker": 'auto',<br />"poolMinSize": 4,<br />"poolMaxSize": 16<br />}                                                                                                                                                         | {<br />processType?: WorkerProcessType;<br />startDelay?: number;<br />elementAddDelay?: number;<br />elementsPerWorker?: number \| 'auto' \| 'all';<br />poolMinSize?: number;<br />poolMaxSize?: number;<br />resourceLimits?: ResourceLimits;<br />}                                                                                                                                                                                                                                                                                                                                                                                                 | Worker configuration section:<br />- _processType_: worker threads process type (`workerSet`/`fixedPool`/`dynamicPool`)<br />- _startDelay_: milliseconds to wait at worker threads startup (only for `workerSet` worker threads process type)<br />- _elementAddDelay_: milliseconds to wait between charging station add<br />- _elementsPerWorker_: number of charging stations per worker threads for the `workerSet` process type (`auto` means (number of stations) / (number of CPUs) \* 1.5 if (number of stations) > (number of CPUs), otherwise 1; `all` means a unique worker will run all charging stations)<br />- _poolMinSize_: worker threads pool minimum number of threads</br >- _poolMaxSize_: worker threads pool maximum number of threads<br />- _resourceLimits_: worker threads [resource limits](https://nodejs.org/api/worker_threads.html#new-workerfilename-options) object option                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
index 2ec0f4b949ca6cf7747c161430219c0f16b198ee..ec10e0ffa3bdfada20297d7ae017f3abe631c573 100644 (file)
@@ -286,11 +286,18 @@ export const StationTemplateUrlSchema = z
   .strict()
 
 /**
- * Supervision URL — a URL that the `ws` WebSocket client can dial: scheme
- * `ws:`, `wss:`, `http:`, `https:` or `ws+unix:`, no fragment identifier, and
- * a socket path for `ws+unix:`. A scheme-less `host:port/path` string parses
- * as a URL with the host as its scheme, so syntax validation alone would
- * accept a value that fails at connection time.
+ * Supervision URL — a URL that the `ws` WebSocket client can dial once the
+ * charging station identity is appended to it: scheme `ws:`, `wss:`, `http:`,
+ * `https:` or `ws+unix:`; no fragment delimiter (`#`, even with an empty
+ * fragment) and no query delimiter (`?`), since the appended identity would
+ * end up in the fragment or the query; and for `ws+unix:` a socket path other
+ * than `/`, then exactly one `:`, then an empty request path or one starting
+ * with `/` (`ws` splits the pathname at `:`, so without the separator the
+ * identity is appended to the socket path, and a request path without a
+ * leading `/` is not a valid HTTP request target). A
+ * scheme-less `host:port/path` string parses as a URL with the host as its
+ * scheme, so syntax validation alone would accept a value that fails at
+ * connection time.
  * Shared by the configuration file and the UI request payload gate so a value
  * is accepted by both or neither.
  */
@@ -299,19 +306,32 @@ export const SupervisionUrlSchema = z
     error: 'must be a WebSocket URL (ws://, wss://, http://, https:// or ws+unix://)',
     protocol: /^(?:wss?|https?|ws\+unix)$/,
   })
-  .refine(value => !URL.canParse(value) || new URL(value).hash === '', {
+  .refine(value => !URL.canParse(value) || !new URL(value).href.includes('#'), {
     message: 'must not contain a fragment identifier (#...)',
   })
+  .refine(value => !URL.canParse(value) || !new URL(value).href.includes('?'), {
+    message: 'must not contain a query string (?...)',
+  })
   .refine(
     value => {
       if (!URL.canParse(value)) {
         return true
       }
       const url = new URL(value)
-      return url.protocol !== 'ws+unix:' || url.pathname !== ''
+      if (url.protocol !== 'ws+unix:') {
+        return true
+      }
+      const [socketPath, ...requestPath] = url.pathname.split(':')
+      return (
+        requestPath.length === 1 &&
+        socketPath !== '' &&
+        socketPath !== '/' &&
+        (requestPath[0] === '' || requestPath[0].startsWith('/'))
+      )
     },
     {
-      message: 'ws+unix:// URLs must include the socket path',
+      message:
+        "ws+unix:// URLs must include the socket path, then exactly one ':' before a request path starting with '/' (ws+unix:///path/to/socket.sock:/path)",
     }
   )
 
index 977c5d96a9b5d05edfc51e65bfe2ebf6826c8969..524338b157343221bec3309df16658d905668469 100644 (file)
@@ -860,8 +860,11 @@ await describe('ConfigurationSchema', async () => {
       'wss://localhost:8010/OCPP16',
       'http://localhost:8010/OCPP16',
       'https://localhost:8010/OCPP16',
-      'ws+unix:///tmp/simulator.sock',
       'ws+unix:///tmp/simulator.sock:/OCPP16',
+      'ws+unix:///tmp/simulator.sock:/',
+      'ws+unix:///tmp/simulator.sock:',
+      'ws+unix:s:/OCPP16',
+      'ws+unix:./s:/OCPP16',
       'ws:///OCPP16',
     ]
     const rejectedUrls = [
@@ -888,16 +891,39 @@ await describe('ConfigurationSchema', async () => {
       }
     })
 
-    await it('should reject a URL with a fragment identifier', () => {
-      const result = SupervisionUrlSchema.safeParse('ws://localhost:8010/OCPP16#fragment')
-      if (result.success) {
-        assert.fail('Expected a URL with a fragment identifier to be rejected')
+    await it('should reject a URL with a fragment delimiter, even with an empty fragment', () => {
+      for (const url of ['ws://localhost:8010/OCPP16#fragment', 'ws://localhost:8010/OCPP16#']) {
+        const result = SupervisionUrlSchema.safeParse(url)
+        if (result.success) {
+          assert.fail(`Expected '${url}' to be rejected`)
+        }
+        assert.match(result.error.issues[0].message, /fragment identifier/)
+      }
+    })
+
+    await it('should reject a URL with a query string delimiter, even with an empty query', () => {
+      for (const url of [
+        'ws://localhost:8010/OCPP16?token=secret',
+        'ws://localhost:8010/OCPP16?',
+      ]) {
+        const result = SupervisionUrlSchema.safeParse(url)
+        if (result.success) {
+          assert.fail(`Expected '${url}' to be rejected`)
+        }
+        assert.match(result.error.issues[0].message, /query string/)
       }
-      assert.match(result.error.issues[0].message, /fragment identifier/)
     })
 
-    await it('should reject a ws+unix URL without a socket path', () => {
-      for (const url of ['ws+unix://', 'ws+unix:']) {
+    await it("should reject a ws+unix URL without a socket path, exactly one ':' and a request path starting with '/'", () => {
+      for (const url of [
+        'ws+unix://',
+        'ws+unix:',
+        'ws+unix:///tmp/simulator.sock',
+        'ws+unix:///:/OCPP16',
+        'ws+unix:///tmp/simulator.sock:/OC:PP16',
+        'ws+unix:///tmp/simulator.sock:OCPP16',
+        'ws+unix:s:OCPP16',
+      ]) {
         const result = SupervisionUrlSchema.safeParse(url)
         if (result.success) {
           assert.fail(`Expected '${url}' to be rejected`)
@@ -906,27 +932,49 @@ await describe('ConfigurationSchema', async () => {
       }
     })
 
-    await it('should match the URLs accepted by the ws client', () => {
+    await it('should match the connection URLs the ws client can dial', () => {
       const edgeCaseUrls = [
         'ws://localhost:8010/OCPP16#fragment',
         'wss://localhost:8010/OCPP16#',
         'ws+unix://',
         'ws+unix:',
+        'ws+unix:///tmp/simulator.sock',
+      ]
+      // URLs the ws client accepts but that do not reach the intended
+      // endpoint once the identity is appended (checked against real servers).
+      const wrongEndpointUrls = [
+        'ws://localhost:8010/OCPP16?token=secret',
+        'ws://localhost:8010/OCPP16?',
+        'ws+unix:///:/OCPP16',
+        'ws+unix:///tmp/simulator.sock:/OC:PP16',
+        'ws+unix:s:OCPP16',
       ]
+      for (const url of wrongEndpointUrls) {
+        assert.strictEqual(SupervisionUrlSchema.safeParse(url).success, false, url)
+      }
       for (const url of [
         ...acceptedUrls,
         ...rejectedUrls.filter(url => URL.canParse(url)),
         ...edgeCaseUrls,
       ]) {
-        let wsAccepts = true
+        // The charging station dials the supervision URL with its identity
+        // appended (see ChargingStation.wsConnectionUrl).
+        const connectionUrl = `${url}${url.endsWith('/') ? '' : '/'}CS-1`
+        let wsCanDial = true
         try {
-          const webSocket = new WebSocket(url)
+          const webSocket = new WebSocket(connectionUrl)
           webSocket.on('error', () => undefined)
           webSocket.terminate()
+          // ws splits a ws+unix pathname at ':' into socket path and request
+          // path; without ':' the identity ends up in the socket path.
+          const { pathname, protocol } = new URL(connectionUrl)
+          if (protocol === 'ws+unix:' && pathname.indexOf(':') <= 0) {
+            wsCanDial = false
+          }
         } catch {
-          wsAccepts = false
+          wsCanDial = false
         }
-        assert.strictEqual(SupervisionUrlSchema.safeParse(url).success, wsAccepts, url)
+        assert.strictEqual(SupervisionUrlSchema.safeParse(url).success, wsCanDial, url)
       }
     })
   })