One EV Charging Station Management System, with a web interface in front of it: a C# HTTP backend built on Hermod, and a frontend of HTML, SCSS and TypeScript bundled by webpack and embedded into the assembly - so the CSMS is one binary to deploy and needs nothing installed beside it.
Below it is WWCP_Node: what every one of these programs is before it is anything in particular - the log, the configuration file, name resolution and the time, a certificate store, the accounts, and the HTTP server with the web interface behind it. The vehicle of EV is one of those with a battery, ChargingStation one with EVSEs; this CSMS is one with a server on a port of its own for the charging stations, the OCPI endpoints its roaming partners call, and the OCPP node it speaks through.
Nothing is rendered on the server. The browser loads one bundle and talks to the CSMS over a JSON API and one Server-Sent Events stream.
browser ── GET / the SPA stub and the bundle
── POST /api/v1/auth/login the session cookie
── GET /api/v1/configuration what the CSMS is made of
── GET /api/v1/logs what happened up to now
── GET /api/v1/events and everything from now on (SSE)
── /ext/... the HTTPExt API: accounts, API keys
A CSMS sits above the charging stations and the local controllers that dial into it, and is the thing they are all pointed at. That is what the web interface is for: it is the one place where somebody can see which of them got in, which were turned away and why, without reading a log file over somebody else's shoulder.
Beside that it is a charge point operator in OCPI: the e-mobility service providers whose customers charge at those stations are peered with it, it publishes the locations its stations stand at, and it takes what the partners push. Two protocols and one box - separate ports, separate identities, separate stores - and what ties them together is that an operator with no stations has nothing to publish and an operator with no partners has nobody to publish it to.
This is built the same way as ChargingStation and LocalController. The DNS and NTS configuration, the clock and the event log are the node's below, and so the very code the station runs; the charging station server, the certificate and trust stores and the station logins are the same thing done at the other end of the same connection.
A CSMS is the back end of an estate: the people who read it are not the people
who configure it, the machines that call it are not people at all, and both
outlive any one of its operators. So the HTTP server of this CSMS carries
Hermod's HTTPExtAPI - accounts, groups, organizations and API keys, kept in a
directory of its own - at /ext, beside the JSON API at /api and the web
interface at /.
The vehicle, the charging station and the local controller sign in against the
same thing, and their roles are groups in it with names that overlap on purpose:
all four have systemadmin and viewer, three of them have cpo. So handing
one HTTPExtAPI to several of them makes one sign-in open all of them, with
each still deciding for itself what a role permits - which is what
EVChargingTestEnvironment
does with --shared.
One HTTP server, one port, three things registered on it: the accounts and the
web interface by the node below, and the JSON API - the routes every node
answers, the node's NodeHTTPAPI, with the CSMS's own on top: the charging
station server and OCPI. A request goes to the most specific of them, so the
single-page-application catch-all only ever gets what neither of the other two
claims - AnUnknownAPIPathAnswersJSONAndNotTheStub says so.
var csms = new CSMS(HTTPPort: IPPort.Parse(2351));
csms.HTTPServer // the one server everything is registered within
csms.ExtAPI // the accounts at /ext
csms.API // the JSON API at /api: the node's, and the CSMS's routes
csms.Node // the OCPP 2.1 CSMS nodeThe OCPP node's own HTTP APIs are switched off on purpose. Left alone an
ACSMSNode builds a second HTTP server and a second HTTPExtAPI on a port it
picks itself; one CSMS should be one address to point a browser at, so the
server and the HTTPExt API are the node's below, made where the listening
address, the port and the moment of starting are decided, and the OCPP node is
handed a role rather than a socket.
| Page | What it changes | Permission |
|---|---|---|
| Configuration | nothing - it answers "what am I running" | configuration:read |
| DNS client | the name servers, how they are asked and what each is held to; a test lookup, of all of them or of one | dns:edit, dns:run |
| NTS client | the time servers of the group, what it and each of them is held to; a synchronisation, and a test of each server | nts:edit, nts:run |
| SSH server | whether the command line is served over SSH, on which port, and whether passwords open it - in effect at once; its host key, who is connected, the keys of the accounts and what it offers | ssh:edit |
| Certificate store | the roots and the server certificates this CSMS believes, and what each is for - certificates alone, no private key | certificates:edit |
| Identities | who this CSMS is as a client, each with its private key: its TLS identities | certificates:edit |
| Charging station server | the port, TLS, the security profiles it accepts | stations:edit |
| Server certificates | the keys and chains this CSMS presents | certificates:edit |
| Client trust | the chains a station's certificate may come from | certificates:edit |
| Station logins | who may sign in, and with what | stations:edit |
| Certificate signing | where the CSRs of the charging stations go - the EST server of a PKI - the certificates the stations have, and what became of the latest; a station asked for a new certificate by hand; what the stations asked of the OCSP responder | certificates:edit |
| Station roots | the root certificates the stations are installed - by hand, on every station connected, or on every station that starts without one - what a station has, and a certificate deleted from it | certificates:edit |
| OCPI | nothing - who this operator is and where its partners find it | roaming:read |
| Roaming partners | the EMSPs it is peered with, and the peering itself | roaming:edit |
| Locations | the charging locations it publishes | locations:edit |
| Roaming data | nothing - what travels between it and its partners | roaming:read |
| Logs | nothing - it reads | anybody signed in |
Looking at a page takes read on the resource in its column: dns:read for
the DNS client, stations:read for the charging station server. The clock, the
log and the event stream are for anybody signed in. Which roles hold what is
under "Who may open it" below.
Everything on the DNS and NTS pages takes effect the moment it is saved, for
everything inside the CSMS that resolves a name or reads a clock, and is written
to configuration.json in the same breath - the file first, because a change
that was applied but not written down disappears at the next start without
anybody noticing.
The node below reads the sections every one of these programs has - dns,
nts and certificates - and the CSMS reads its own - ocpp, ocppServer,
ocpi and certificateSigning - from the same document; each passes over
what is the other's.
Beside the file the node keeps its certificate store, in certificates/, and
what it believed each server with, in known-servers.json. What the CSMS
presents to the charging stations and believes of them is its charging station
server's, in stores of its own beside the file: the keys it presents in
ocpp-server-keys/, and the chains it trusts them by in ocpp-client-trust/.
What this CSMS says it is in OCPP - its node id, vendor, model, serial number -
is read from the ocpp section of that file at the start and is deliberately
not changeable while running: an identification is what a charging station
knows this CSMS by, and changing it under live connections would not rename the
CSMS, it would make it a second one nobody is talking to.
The same goes for the ocpi section - the country code, the party
identification, the business name and the versions offered. Those are what every
roaming partner wrote into its credentials, and they have nothing to do with the
OCPP identification above. The partners themselves are not in that file: the
OCPI library keeps them, and what they sent, in append-only files of its own
below an ocpi/ directory beside the configuration, one set per version, and
reads them back at every start. A partner is added or removed only once its
file has it: where that file cannot be written, the Roaming partners page is
answered 500 with why, and nothing changes. So it is with a registration:
where the file cannot take the token the partner is to call back with, nothing
is sent. Where the partner accepted and the file refused its answer, the
registration is in effect all the same - the partner uses the new tokens
already - and is written down with the next change the file takes, or when the
CSMS stops; repair the file before then, or the next start will not know it,
which the log says. Both are answered 500 with why. A partner registering here
while its file refuses is answered OCPI 3000 with HTTP 500, and the token it
came with stays valid.
An OCPI request whose handling fails is answered OCPI 3000 with HTTP 500, a
message that says nothing of this CSMS, and the request and correlation ids the
caller came with - or new ones - to quote. What failed is an error in the log,
tagged ocpi and http: what was asked, by which partner or from which
address, the ids, and what was thrown; not the headers, which carry the token.
A Basic authentication whose password is no TOTP, which is what a scanner
sends, is turned away as an unknown token, and no error.
A token no partner was given is answered 401, OCPI 2000 "Unknown access
token!", at every path of every version, before the path is asked - the
versions, the version details, the credentials and every module alike. No
token at all is not that: the versions and the version details stay open, so
that a partner finds the credentials endpoint before it registers there with
the token it was given. With locationsAsOpenData or tariffsAsOpenData in
the ocpi section the locations or tariffs are anybody's, and a made-up token
reads them as no token does; everything else still refuses it.
A partner is called where it says it is - the versions URL it hands in with its
credentials, and every endpoint its versions name - with its token in the
header, and so on the public internet alone: this machine, the networks beside
it and a cloud's metadata service are refused, whatever a name resolves to.
A partner on the same site, or a bench with both ends on one machine, is
allowed by remoteNetworks in the ocpi section, rules read in their order
before the rest are refused:
"ocpi": { "remoteNetworks": [ "192.168.0.0/16 => ALLOW", "127.0.0.0/8 => ALLOW", "::1 => ALLOW" ] }A rule may refuse as well ("203.0.113.7 => DENY"), and "0.0.0.0/0 => ALLOW"
with "::/0 => ALLOW" switches it off. A rule that is none stops the start
with why. The log says at every start which rules there are.
Two sets of stores, because they face two different ways. What this CSMS
presents to the charging stations, and the chains it lets them in by, are its
charging station server's: ocpp-server-keys/ and ocpp-client-trust/, the
Server certificates and the Accepted chains pages. Everything else it believes
is in the node's store below - WWCP_Node's CertificateStore, a directory of
files with an index.json beside them, in certificates/ beside the
configuration file - and is addressed by a short handle rather than by a path.
The Certificate store and Identities pages manage it, and so do CSMSCLI's
--import-certificate and --list-certificates.
A CSMS keeps six of the node's eleven kinds, CSMS.CertificateKinds: the
vehicle's credentials are no business of the back end's, and neither is a
clientRoot - the clients of a CSMS are its charging stations, and they are
let in by the Accepted chains. A client root kept before stays in the index as
it was, switched on or off and under its label.
A root is what this CSMS believes. tlsRoot is for a server it connects
to - a time server, or a name server over TLS or HTTPS - and is believed beside
the roots of the machine it runs on, not instead of them. v2gRoot, moRoot
and oemRoot are ISO 15118's, for a station's chain, a contract's and a
provisioning chain. They are kept apart because one bag of roots would let an
OEM root vouch for a contract, and they are kept for what is to come: nothing
in this CSMS checks a chain against them yet. Any number of each may be
switched on at once, and all of them are believed.
A server certificate - tlsServer - is what a server this CSMS connects to
shows, kept so that the server can be held to it by its fingerprint, and never
with a private key, which would be that server's key in the wrong place. The
page shows these as a third group, what the CSMS recognises. A
tlsIdentity - who the CSMS is as a client - is kept as well, and nothing in
the CSMS uses it yet. A client identity is offered no listener; a CSMS keeps no
tlsServerIdentity, the node's kind for a listener of its own, since what it
shows the charging stations is its charging station server's. Any certificate
may be marked with a usage made up, for a configuration or code to name later.
One certificate may be kept as several kinds - a TLS root and a kind made up, say. The store is looked after on two pages. Certificate store keeps certificates alone, with no private key: the roots and the server certificates; an upload there leaves a key in the box out, and says where it goes. Identities keeps who the CSMS is as a client, each with its key: the TLS identities. Each page has three tabs: by usage, every certificate once, and the upload, where certificates are pasted or files dropped and every one of them is kept as every kind ticked.
A TLS root and a server certificate are told what they are for: the time
servers (nts), the name servers (dns), or - with nothing said - every use.
The page asks at the upload and again with Uses, because one root may vouch
for both, and a root kept for the name servers alone vouches for no time. What
a server is held to is said in its own entry, on the NTS and the DNS page,
where its dialog offers the ones kept for it.
The store holds private keys unencrypted - of these kinds only a
tlsIdentity has one: a PKCS#12 is opened with its password once, at import,
and written back without one. The file system is what guards them, and the
CSMS says so at every start and at every import. A file copied into the
directory by hand is adopted at the next start, or at once by
POST /api/v1/certificates/reload.
What holds a secret is for the account the CSMS runs under alone:
ocpp-server-keys/, ocpp-stations.json with the stations' passwords and
ocpi/ with the partners' tokens, beside the node's own accounts/, logs/,
certificates/ and ssh/. On Unix that is 0700 for a directory and 0600 for a
file; on Windows an access control list of its own, nothing inherited from the
folder above, that grants this account, SYSTEM and the administrators - what a
folder at the root of a drive would otherwise grant every authenticated user.
What an earlier version left open is closed at the next start. A file system
that can do neither, a mounted share say, is a warning in the log, and the CSMS
runs all the same.
The Server certificates page has three tabs as well: the keys and their signing
requests, every certificate once by name or by fingerprint with the key it is
for, and an upload that takes any number of certificates in at once - renewals
for several keys, say - each under the key it belongs to, which the CSMS reads
from the certificate itself (POST …/ocpp-server/certificates/inspect and
…/upload). A private key that comes along in a file is left out of the box
and its certificate refused: the key is made here and never arrives.
The charging station server's keys and chains are kept whole. A change their
files cannot take is answered 500 with why, and what is in effect stays as it
was: a key whose files could not all be written - itself, its signing request
and its description - is taken away again, and so is a chain whose description
could not be written after its certificates. A certificate is written beside
the one it replaces and moved over it, and so is what is said of a chain when
it is renamed or switched on or off, so that a renewal or a change the disk
runs out in leaves what was there whole. A key or a chain is taken away whole
or not at all: its files are set aside, as *.removed, before any of them is
deleted, and put back where one of them cannot be - held open by somebody, as
Windows keeps a file then. A file that cannot be put back, or deleted once all
are aside, is left over under its *.removed name, which the next start does
not read, and said in the log.
A charging station asks for a certificate with SignCertificate, OCPP 2.0.1
and 2.1, A02 and A03 - its own, which it signs in with at security profile 3,
or the SECC certificate of ISO 15118-2. The CSMS sends the CSR on to the EST
server of a PKI, RFC 7030 - the PKI
of OpenChargingCloud, or any other - and hands the certificate that comes back
to the station with CertificateSigned, with the CAs above it but the root:
"certificateSigning": {
"chargingStationCertificate": "https://pki.example.org/.well-known/est/stations",
"v2gCertificate": "https://pki.example.org/.well-known/est/secc",
"apiKey": "<an API key of the PKI>",
"renewDaysBefore": 30,
"ocspResponders": [ "http://ocsp.example.com/" ]
}An address is where EST is, with its label: the CSR goes to its
simpleenroll, and the CAs come from its cacerts. A kind of certificate
without one is not signed, and the station is told so. The API key is that of
an account of the PKI that may sign certificates of servers and clients - the
registrar role of the PKI of OpenChargingCloud - and goes over HTTPS only, or
to the loopback address; plain HTTP to anywhere else is refused, since the key
and the certificates would travel in the clear. The PKI's TLS certificate is
judged as the name and the time servers' are: by the machine's roots and the
TLS roots of the certificate store for pki.
The station is answered at once - Accepted, or Rejected with why - and the
certificate follows once the PKI has signed it. A CSR whose signature does not
hold is rejected, and so is a station's own certificate for any common name but
the station's identity: a station would otherwise be signed a certificate to
sign in as another. A SECC certificate names the SECC, which the CSMS does not
know, and what it may be is the PKI's to say. One request of each kind for each
station at a time. The Certificate signing page, and GET and PUT
/api/v1/configuration/certificate-signing, say where the CSRs go and what
became of the latest hundred: signed, rejected, not signed by the PKI, or not
taken by the station. The API key is never shown again, only whether there is
one; PUT changes what it names, removes what it sets to null, and keeps the
rest.
The CSMS keeps the certificate of each kind of each station it knows of in
ocpp-station-certificates.json, beside the configuration file: the one it had
signed and the station took, or the one a station signed in with at security
profile 3 - the later of the two where it knows both. A station whose
certificate runs out within renewDaysBefore days, 30 where nothing is said, is
asked with TriggerMessage for a SignCertificate of that kind, OCPP 2.0.1 and
2.1, A02: once it was accepted with its BootNotification, and every day while
it stays connected. Its CSR goes the way every other does, and the certificate
signed is the one known from then on. A station is asked once a day at most for
the same certificate, so that one that says no, or never sends its CSR, is
asked again the next day rather than at every reconnection; a kind of
certificate the CSMS sends to no PKI is not asked for at all. 0 is never.
A certificate is never renewed before half of its life is over: one that holds
for less than twice renewDaysBefore - ten days from a PKI, with the thirty of
the default - is renewed halfway, or every new certificate would be due the day
it was signed and the station asked for another every day. The page marks such
a certificate halfway. Of a certificate noted before its first day was kept,
only renewDaysBefore counts, until the station signs in with it again.
The page shows the certificates, until when they hold, from when they are
renewed, which are due, which stations are connected, and what each said when
it was last asked; Renew now, or
POST /api/v1/configuration/certificate-signing/renew with
{ "station", "certificateType" }, asks a connected station at once, whatever is
due - its first certificate, for a station that signs in with a password. It
answers what the station said, 409 where the station is not connected.
Forget, or
DELETE /api/v1/configuration/certificate-signing/certificates/{station}/{certificateType},
has the CSMS forget a certificate, which is not renewed any more until the
station signs in with it again or is signed a new one; a station removed takes
its certificates with it.
A station asks whether a certificate is revoked with GetCertificateStatus,
OCPP 2.0.1 and 2.1, M06 - above all the CAs of its SECC certificate, whose OCSP
answers it hands to the EV in ISO 15118-2, and which has no way to the OCSP
responder of the PKI but through the CSMS. The CSMS makes an OCSP request of
RFC 6960 of what the station names - the hashes of the issuer and the serial
number - sends it to the responder the station names, and hands the answer
back as it came, DER in Base64, for the station and the EV to judge by its
signature; one longer than the 18000 characters every station takes is not.
The responder is the station's word, and the CSMS asks it from inside its
network, so it asks only one on the host of an EST server above - at that
server's port, or at 80 or 443, as the OCSP responder of the PKI of
OpenChargingCloud is at <its public address>/api/v1/pki/ocsp - or one that
begins with an address of ocspResponders. Any other request, and one that is
no OCSP request, is answered Failed, as is one the responder does not answer
successfully. The Certificate signing page shows the latest hundred: the
station, the serial number, the responder, and what it said - good, revoked or
unknown - and until when.
Where the certificate revoked is the station's own - one the CSMS signed for
it, or one it signed in with, known by its serial number - the station is asked
for a new one at once, with TriggerMessage, and not when the old one would
run out.
A station trusts what it is installed, by kind: the root above the CSMS's TLS
certificate, which it signs in to it by, the V2G root of ISO 15118, the roots of
mobility operators, of manufacturers and of OEMs. The Station roots page
keeps the roots the CSMS installs, in ocpp-station-roots.json beside the
configuration file - a CA that signed itself, of one of the kinds of
InstallCertificate - and installs them, OCPP 2.0.1 and 2.1, M05: on a station,
on every station connected, or, for a root marked as one every station is to
have, on every station that is accepted with its BootNotification and does
not have it. The new root of a rollover is on the stations that way before the
first certificate below it is.
Take the roots of the PKI adds the CAs the EST servers name in cacerts
that signed themselves: the one above the V2G certificates as the V2G root, and
the one above the CSMS's own TLS certificate as its root.
The page asks a station what it has, GetInstalledCertificateIds, M03, names
the roots of the CSMS among it, and deletes a certificate from it,
DeleteCertificate, M04. The latest hundred changes are shown, and logged.
Behind it, /api/v1/configuration/station-roots:
GET |
the roots, the stations connected, and the latest changes |
POST |
{ "pem", "certificateType", "everywhere" }: a root added |
POST .../from-pki |
the roots of the PKI added |
PUT .../{type}/{id} |
{ "everywhere" } |
DELETE .../{type}/{id} |
no longer installed; it stays where it is |
POST .../{type}/{id}/install |
{ "station" }: on that station, or on every one connected where none is named |
GET .../stations/{station} |
what the station has |
POST .../stations/{station}/delete |
{ "certificateType", "hashAlgorithm", "issuerNameHash", "issuerKeyHash", "serialNumber" } |
Reading is at the stations' permission, changing at the certificates'.
From the repository that has this one as a submodule (CSMSCLI):
dotnet run --project CSMSCLI
At the first start there are no accounts, so the CSMS makes one up - root,
under accounts/ beside the configuration - and prints its password once:
┌─ First start: there were no accounts, so one was made up for you ─────────
│ user root
│ password QBDD77Lc7HseB-xORuuw8RpX
│ It is shown here once and kept only as a hash. Write it down.
└───────────────────────────────────────────────────────────────────────────
Then open http://127.0.0.1:2351/ and sign in. Signing in happens at Hermod's
HTTPExt API, mounted under /ext - the same door the charging station and the
local controller use.
Recommended for the first start: bring root your own SSH key, for the
command line over SSH -
dotnet run --project CSMSCLI -- --authorize-ssh-key root=C:\Users\you\.ssh\id_ed25519.pub
Without it, the first start makes up a key pair for root and prints its
private key once, below the password, from -----BEGIN OPENSSH PRIVATE KEY-----
to the END line, to be saved as a file only you can read. See
CSMSCLI for signing in with it.
Whoever signs in over SSH gets the command line of the CSMS, whichever program
runs it: the commands every node has, syncNTS among them, and beside them
what only a CSMS can be told - stationKeys, stationQueryDNS,
stationSyncNTS, connections, and use, which chooses a connected charging
station for the OCPP library's operator commands: reset, remoteStart,
changeAvailability, getBaseReport and the rest. Those need the role that
may run the stations, cpo or an administrator's (stations:run), and are
asked for it every time they run. They are this library's, in
CSMS/CommandLine/, and not the program's, so that a CSMS in the test
environment can be told the same as one run by CSMSCLI.
Port 2351 and not 2348 or 2350: an OpenChargingCloud charging station uses 2348 and 2349 and a local controller 2350, and all three are routinely tried out on the same bench.
dotnet build builds the frontend too: CSMS.csproj runs npm ci (only when
Frontend/node_modules is missing) and npm run build (only when something
changed under Frontend/src, or under libs/WWCP_Node/Frontend/src, which
holds what the web interface of every kind of node shares and is bundled in as
@node/...), then embeds every file of Frontend/dist as a manifest resource
named cloud.charging.open.CSMS.HTTPRoot.<path> - which is what Hermod's
EmbeddedContentSource reads and MapSinglePageApplication serves.
dotnet build the whole thing
dotnet build -p:SkipFrontendBuild=true backend only, reusing the existing dist/ -
and where there is none, no web interface
at all, which is a warning and not an error
npm run watch (in Frontend/) rebuild the bundle as it is edited
npm run typecheck (in Frontend/) tsc --noEmit
While editing the frontend, start the CSMS with --frontend libs/CSMS/CSMS/Frontend/dist so that it serves the directory npm run watch
writes into: a reload in the browser then shows the change, without rebuilding
the C# side.
dotnet test libs/CSMS/CSMSTests
They start real CSMSs and talk to them over HTTP the way the browser does: the
bundle is served, the sign-in works, a change to the name servers reaches both
the shared DNS client and the file, the log filters, the event stream delivers,
the certificate store is filled, told what a root is for and told again, and a
CSMS that is told to stop stops. That much every node has to answer alike, and
it is WWCP_Node's conformance suite, NodeConformanceTests in
WWCP_Node_TestKit, which CSMSConformance runs against a CSMS - see
WWCP_Node's README.
The rest is what only a CSMS does: its charging station server, OCPI, its OCPP
identity, the roles and resources that are its own and the kinds its store
keeps. CSMSAccessTests reads the one permission every route of the API asks
for off the refusal an account in no role is given. What the node below does
on its own - the log, the clock and the time servers, the configuration file
and the node's sections of it, the start, the certificate store, the accounts'
roles and the ports - is tested in WWCP_Node's own WWCP_Node_Tests, against a
node of no particular kind, and not again here. What is left of it here is what a CSMS says: its
section of the file beside the node's, its OCPP identity and defaults, and the
first line of its log file.
Each test gets a CSMS of its own, on a port WWCP_Node_TestKit's TestPorts
hands out once in a test run, and with its own directory for what a CSMS
writes: its accounts, its configuration file, and beside that the stores of the
charging station server - so they neither fight with each other nor with a
CSMS somebody has running on 2351 while they work.
A port a test's own back end - a roaming partner - is started on is held
rather than found free: a ClosedPort of WWCP_Node_TestKit, bound and not
listening, so that no other test run on the same machine can be found there in
the moment between finding the port and using it. WWCP_Node_Tests asks it what
the tests stand on: refused, nobody else can listen on it, and handed over it
takes a back end.
They never touch the network. The configuration written before each CSMS is built switches the time client off, which is what stops the clock check from being scheduled at all, and the DNS client is asked what it is configured as - or a name server on the loopback address that never answers, its port held for as long as it is asked. A test suite that needs a name server to answer is a test suite that fails on a train.
CSMS takes a TimeProvider as its last constructor parameter and hands it to
everything of its own that asks what time it is: the timestamp of every log
entry, CreatedAt, the uptime the status resource reports, and the sessions -
through Hermod's SessionStore, which takes one too. The system clock by
default; an NTS-disciplined or a fake one where a test says so.
It is assigned first - by the node below, before the event log is built - because the log stamps its entries with it: a clock set afterwards would leave the log reading the system one, which is a log that cannot be held against anything.
sealed class FixedClock(DateTimeOffset Start) : TimeProvider
{
public DateTimeOffset Now { get; set; } = Start;
public override DateTimeOffset GetUtcNow() => Now;
}
var clock = new FixedClock(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero));
var csms = new CSMS(TimeProvider: clock);
csms.Sessions.TryLogin("root", password, out var session); // 1 live session
clock.Now = clock.Now.AddHours(13); // past the 12 hour idle timeout
var gone = csms.Sessions.Count; // 0The clock is never set from NTS. Every fifteen minutes the CSMS asks its time servers what time it is, measures the difference and reports it - and leaves its own clock exactly where it was. Everything connected to this CSMS is stamped against this clock, so a jump backwards would put two meter readings out of order in a record written somewhere else entirely, with nothing in it to say why.
It asks a group, not a server. The nts section names its groups of time
servers under groups, and the one it checks its clock against is the legal
one, nts.groups.legal. Its servers are a list, by default the PTB's four, of
which minServers - two - have to answer before the group has a time at all. One host being rebooted no longer leaves this CSMS without a
check, and two servers that agree catch what one server cannot: one that is
wrong rather than absent. What the check reports is what the servers that
answered and authenticated agree on, with a line for each of them, so a failure
says which of the four failed and how.
Every key of a group, and what it is when absent:
| Key | Default | |
|---|---|---|
enabled |
true |
whether to ask at all |
servers |
the PTB's four | a list, see below |
minServers |
2, or all of them when fewer |
how many must answer for the group to have a time |
maxDeviationSeconds |
60 |
how far apart they may be before it is written down |
hostname |
- | one server instead of a list |
ntsKEPort, ntpPort |
4460, 123 |
for that one server |
timeoutSeconds |
10 |
per request |
checkEverySeconds |
900 |
how often the clock is checked |
errorLogging |
5 |
failed checks in a row before that is written down |
maxErrors |
10, 50 for "local" |
failed checks in a row before what rests on the group is limited |
legalTimeAuthority |
- | who the operator says stands behind it |
legalTimeToleranceSeconds |
1 |
how far off the clock may be |
legalTimeMaxAgeSeconds |
3600 |
how old the last check may be |
Servers sharing a priority are one band and are asked together; a lower
priority is asked first. The four it asks by default share one, because they are
peers - putting them in separate bands would say something about them that is not
true. An entry may be a bare host name or an object saying more:
{ "hostname": "time.local", "priority": 0, "ntsKEPort": 4460, "enabled": true }.
Servers that disagree by more than maxDeviationSeconds are written down
rather than acted on. The disagreement belongs in the log, and the time is still a
time.
A group naming a single hostname and no list is a group of one server, held
to a quorum of one, and one asking two of it is refused. A list without minServers is held to two, as the default four are, or
to all of its servers when it has fewer switched on.
A group mentioning neither leaves the servers alone rather than quietly reducing
four to one - the switch on the NTS page sends nothing but enabled - and one
mentioning nothing but minServers or maxDeviationSeconds holds the servers the
CSMS already has to it. A quorum those servers could never reach is refused: at
the start, before anything is asked, and over the API, before anything is written
into the file. A new interval, and switching NTS off or on, reach a running check
at once.
A host name written back into the file carries the root label -
ptbtime1.ptb.de. - because that is the absolute form it was parsed into, and not
a stray character. What the CSMS prints for somebody to read drops it again.
A section with keys outside of groups - the flat nts section of the files
from before there were groups, "nts": { "enabled": false } say - is refused at
the start, with where they belong: "nts": { "groups": { "legal": { "enabled": false } } }. A save never moves it into a group either; the file is left as it
was. No nts section at all, and "nts": { }, is the legal group of the PTB's
four. More groups - "local", say - are WWCP_Node's to describe, as is what
rests on each of them.
An entry of dns.servers is an address or a host name, as a string or as the
object the DNS page writes the list back in:
{ "address": "9.9.9.9", "port": 853, "transport": "TLS", "queryTimeoutSeconds": 2 }Without a port, the transport's own is used. udp://9.9.9.9:53 is how the log and
the banner name a name server, and not a form the file takes: a file saying it is
refused at the start, and the page refuses it the same way, with the entry named.
GET /api/v1/clock is that measurement, and the one word it never
guesses is "legal": that needs a claim the operator wrote into
nts.groups.legal.legalTimeAuthority, a check against that very server, a recent one, and a
small difference. Any of those missing and the answer says unverified and names
which one in why.
A time server, and a name server asked over TLS or HTTPS, can be held to a
certificate or a root, and the NTS and the DNS page are where that is said: a
server's dialog takes SHA-256 fingerprints one to a line, adds the one the
server showed last or one the certificate store keeps for it with a click, and
says what a mismatch comes to and whether the server is held to what it is
first believed with. Its row says what was made of its certificate the last
time - believed, used although it did not match, or refused, and why - what it
is held to, and when it showed another certificate than before. How a
certificate is judged, what trust on first use learns and what
known-servers.json remembers are the node's, and written down once in
WWCP_Node's README.
Each server can be asked on its own, too. The NTS page tests one time server
step by step, down to the SHA-256 fingerprint of the certificate it showed -
POST /api/v1/configuration/nts/test with a host - and measures the clock
without stepping it. The DNS page asks the name servers the way everything in
the CSMS does, or one of them alone - POST /api/v1/configuration/dns/query
with a server, its place in the list counted from 0 - because the lookup
that fails for everybody is the one where somebody wants to know which server
is not answering. A place with no server at it is an answer that says so, and
something that is not a place is refused rather than read as "all of them".
The whole list goes to the CSMS at every save, so every server goes with what
it is held to, and the pages' ntsServers.ts, dnsServers.ts and pins.ts -
WWCP_Node's, since the DNS and NTS pages are every node's - are where that is
decided and tested: a list sent without the pins of the servers nobody touched
would let go of them, the ones learned on first use included. A name server
switched to a transport that shows no certificate lets go of its pins when it
is saved - the CSMS would refuse them - and its row says so first. Holding a
server to a fingerprint is dns:edit or nts:edit, with the rest of the
server, and so the CPO's: a pin cannot make the CSMS believe a certificate that
chains to nothing this machine or its store holds, and what goes into the store
stays the administrators'.
What a server learns on first use is written into its entry at the first key
exchange or handshake after a save - mostly with the NTS or DNS page still
open, and the list the page sends back was loaded before. So the pages send
every server they loaded with what they showed it held to as well, under
pinsAsShown: their next save keeps what was learned in between, and still
takes away a pin that was shown and removed there. WWCP_Node's conformance
suite asks it of both over the API, as of every kind of node - see "The tests".
Every entry carries a timestamp, a level (debug, info, notice, warning,
error, critical) and any number of tags (http, ocpp, dns, nts, web,
auth, station, ...). The Logs page filters on both - the level counts as a
tag, so critical and ocpp can be picked together. The log is the node's, and
so are its three listeners - the console, the file and the debug bridge; the
entries about the CSMS itself are tagged csms, and a day's file is
csms-2026-09-25.log.
Anything in the CSMS can write to it:
csms.Log.Warning("A charging station was turned away.", "ocpp", "station");What the libraries below write through Illias' DebugX lands there too, tagged
trace plus whatever the CSMS's own table, CSMS.TraceTags, recognises in the
text - OCPP and which side of it a line is about, rather than the vehicle's
ISO 15118 the node's default table leans towards. That works in a debug
build only: Debug.WriteLine carries [Conditional("DEBUG")], so a release
build of those libraries compiles the calls away. --no-trace switches the
bridge off.
The entries are numbered and the number only ever grows. A browser loads a
snapshot from /api/v1/logs, which says how far it reaches, and then applies
everything newer from /api/v1/events - so a reconnect that replays a few cached
events costs bytes and nothing else.
The stream asks, before every entry it sends and at every heartbeat, whether whoever opened it would still be let in. Once the session it was opened with has ended - signed out, expired, or taken back with the account's others - the stream ends too, without the entry, and the browser's next try is answered with a 401. One opened with an API key ends the same way once the key is revoked or has run out, and one opened with Basic auth once its password has changed: it is asked about the password before every entry, as a new request with it is. The stream, like every route that is not the charging station server's or OCPI's, is the node's - see WWCP_Node's README, "The JSON API".
Three places keep it, because they answer different questions. The console
shows it to whoever started the CSMS, at the level they chose; the Logs page
keeps the last two thousand entries; and a LogPath handed to the constructor
writes every entry, down to the debug ones, into one file per UTC day below it.
CSMSCLI does that unless told --no-log-file, since the other two are gone with
the process. A file that cannot be written is said once on stderr, and the file
says how many entries it missed once it can be written again.
A program that reads commands on the same console hands the log a way to write around the line being typed, so that an entry arriving mid-word neither lands inside the command nor waits for it:
csms.ShareConsoleWith(cli.WriteBlock); // line off, entry whole, line backOne answer, and it is Hermod's: the accounts of the HTTPExt API at /ext,
under accounts/, with every password kept as a PBKDF2-SHA256 PHC string and
never in the clear. Signing in happens there; this CSMS's own API only reads
what that door set.
What somebody may do comes from the groups they are in. Each group is one role,
under the same name, and a role is a list of permissions, each an operation on a
resource - written dns:edit. That is the model of every node, described in
WWCP_Node under "Who may sign
in"; the operations are read, edit and run, and the resources are the
node's configuration, dns, nts and certificates and the three a CSMS
adds: stations, the charging station server and which stations may sign in;
locations, the locations it publishes; and roaming, who it is in OCPI and
the partners it is peered with.
| Role | May |
|---|---|
viewer |
read everything: *:read |
cpo |
that, and run the charging stations: change the name and time servers and ask them (dns and nts, edit and run), change the charging stations and the locations (stations:edit, locations:edit), and operate the stations over OCPP from the command line (stations:run) |
systemadmin |
everything this CSMS can be told, the certificates and the roaming included |
The viewer and the administrators are the node's, the CPO is the CSMS's - see
CSMSAccess.cs. The certificates and the roaming are the two resources only
the administrators may change: somebody who can add a certificate authority can
let in a charging station that nobody issued a password to, or make this CSMS
believe a time server nobody else would, and who this operator is peered with
is a contract with somebody else.
The roles section of configuration.json adds roles, or says differently what
one of them may do - and a role there that names a resource this CSMS does not
have stops the start, rather than quietly granting nothing:
"roles": { "support": [ "dns:read", "stations:read" ] }The groups are made at every start rather than only the first, because they are this CSMS's vocabulary and not somebody's data: a group deleted by hand would otherwise leave a role nobody could ever hold again. A group that is none of these, and none the file names, grants nothing - a role this CSMS has never heard of is a role it cannot enforce. Membership is asked of the groups on every request rather than remembered at the sign-in, so taking somebody out of one takes effect on their next request instead of at their next sign-in. The permissions travel to the browser, spelt out resource by resource, so a page can grey out what somebody may not do - a courtesy, not a lock: every request is checked again on arrival.
This software is Open Source under the Affero GPL 3.0 license. We appreciate your participation in this ongoing project, and your help to improve it and the e-mobility ICT in general. If you find bugs, want to request a feature or send us a pull request, feel free to use the normal GitHub features to do so. For this please read the Contributor License Agreement carefully and send us a signed copy or use a similar free and open license.