Skip to content

feat(ocpp-wamp)!: admit a charging station from its upgrade request headers - #154

Open
juherr wants to merge 11 commits into
IZIVIA:devfrom
juherr:juherr/ocpp-toolkit-152
Open

juherr wants to merge 11 commits into
IZIVIA:devfrom
juherr:juherr/ocpp-toolkit-152

Conversation

@juherr

@juherr juherr commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #152; this is stacked on #134 (itself on #128), so the diff also shows their commits, the optional OcppErrorListener of #128 and the 404 refused handshake of #134, and only the last four commits belong here.

A server-level ConnectionAuthorizer (given to OcppWampServer.newServer, WebsocketServer or ServerSetting.authorizer) decides from a ConnectionRequest holding the station id and the headers of its upgrade request, before any handler's accept: a refused or throwing check answers a 401 with a WWW-Authenticate: Basic challenge, and the station is never registered nor notified.

Breaking: WsServerConfig.acceptWebSocketPredicate and the acceptWebSocketPredicate parameter of Undertow become admitWebSocket, which returns a WebSocketUpgradeDecision (or null when the request is not for that endpoint), and CSMS lets the first endpoint claiming the request decide.

The upgrade request is no longer logged with its Authorization header, and the README shows how to check the Basic authentication of a station, comparing bytes so that the binary AuthorizationKey of the OCPP 1.6 Security Whitepaper works as well as the UTF-8 password of OCPP 2.0.1.

Failures handled inside the toolkit only reach SLF4J, so an integrator
cannot report them to a monitoring system.

Add a provider-agnostic event and listener in utils, the one module
the WAMP and SOAP stacks both see. notifySafely isolates the caller
from whatever the listener throws, including an Error such as a
monitoring class missing at runtime. Calling it with ?. builds no event
without a listener.
Report malformed messages, calls without a handler, throwing handlers
and transport failures. The answers sent to the charging station do
not change.

http4k routes whatever escapes ws.onMessage to ws.onError, which makes
it the only place reporting that path. An executor running calls on
the message thread sends an already reported failure back there too,
so the connection remembers it to report it once.
The single catch converting failures to a CALLERROR covers three
stages. Track the current one to tell a payload that cannot be parsed
or serialized from a throwing callback. The CALLERROR does not change.
Report unknown actions, malformed envelopes, throwing callbacks and
responses that cannot be serialized. Failures still propagate to
http4k, so the HTTP answers do not change.
One registration covers the WebSocket and SOAP servers. Document it in
the README, with an example reporting to a monitoring system.
The KDoc and the README said a blocked listener only delays its own
charging station. Malformed messages, rejected calls and WebSocket
errors are reported on the I/O thread shared by many connections, so
it delays every station served by that thread.

Addresses review: IZIVIA#128 (comment)
…uses

The accept predicate tested `filter { ... } != null`, which is true
for any extracted id. A refused station got a 101, fired
onWsConnectHandler and could evict an accepted station whose id
differs only by case, since connection keys are lowercased.

Accept only when a handler accepts, and memoize only non-empty lists,
so an unknown id no longer adds an entry to selectedHandler. The entry
of a station refused later stays: newConnection reads it after
registering the connection, so removing it could fail a handshake in
flight, and the predicate rewrites it before any read.

Answer the refusal with the 404 that OCPP-J asks for (SHOULD, 1.6
section 3.2 and 2.1 part 4), instead of falling through to the HTTP
handler, whose status depended on SOAP sharing the port.

Stations with acceptConnection = false used to connect and now fail
their handshake, and a server with no handler refuses every station.

Fixes IZIVIA#130
@juherr
juherr force-pushed the juherr/ocpp-toolkit-152 branch from 2de9122 to aea5498 Compare November 8, 2026 14:57
…grade request

Security profiles 1 and 2 authenticate a charging station with HTTP
Basic authentication on its upgrade request, which the toolkit could
not check: OcppWampServerHandler.accept only sees the station id.

A ConnectionAuthorizer given to the server now decides from a
ConnectionRequest holding the id and the headers of the request, once
per upgrade and before any handler's accept. The request is a class so
that it can later carry the client certificate of security profile 3
without breaking authorizers, and its toString leaves the headers out.

A refused station gets a 401 with a Basic challenge, and is never
memoized, registered nor notified. Running it first means an
unknown station without valid credentials gets a 401 as well, rather
than a 404 that would tell its id apart.

The authorizer fails closed: one that throws refuses the station. Only
the station id and the exception class are logged, since the headers
and the exception message may carry credentials.

For the same reason, the upgrade request is now logged without its
Authorization header once a station is accepted: the debug log of the
connection attempt and the errors of a new connection printed it.

BREAKING CHANGE: WsServerConfig.acceptWebSocketPredicate and the
acceptWebSocketPredicate parameter of Undertow are replaced by
admitWebSocket, which returns a WebSocketUpgradeDecision, or null when
the request is not for that endpoint, instead of a Boolean.

Refs IZIVIA#152
WebsocketServer takes the ConnectionAuthorizer of its OcppWampServer,
so that a CSMS built on the transport can check the Basic
authentication of its charging stations.

Refs IZIVIA#152
ServerSetting.authorizer reaches the WebSocket server of the setting,
and is ignored for SOAP. When several WebSocket servers share a port,
the first one claiming the upgrade request decides.

Refs IZIVIA#152
@juherr
juherr force-pushed the juherr/ocpp-toolkit-152 branch from aea5498 to 82df971 Compare November 8, 2026 15:04
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ocpp-wamp): admit a charging station from the headers of its upgrade request

1 participant