Files
MijnBesluit/docs/Socketprotocol.md
T
jfandClaude Sonnet 5.5 5446dd4170 Versiehistorie in een tabel, voor site en app
De versiehistorie stond als tekst in zowel de app als de website. Nu staat hij in
tabel version_history, met een beheerpagina (Beheer > Versiehistorie) op de site.
De site leest de tabel rechtstreeks, de app via de nieuwe actie app.versiehistorie.
Startrijen: db/handmatig/22b_versiehistorie_startrijen.sql (als root).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MGLqt1uKfNARQHut9ifcx
2026-10-20 11:47:06 +00:00

180 lines
8.2 KiB
Markdown

# Het socketprotocol van MijnBesluit
De Android- en iOS-app praten met `MijnBesluit.SocketServer` over een eigen TCP-verbinding.
Geen IIS, geen webservice.
## De vorm van een bericht
```
[4 bytes lengte, big-endian][UTF-8 JSON]
```
De lengte staat er dus vóór. De ontvanger weet daardoor exact wanneer een bericht af is.
> **Waarom niet de `**BYE**`-markering uit de RenH-socketserver?**
> Omdat TCP geen berichtgrenzen kent. Wie in de datastroom naar een markering zoekt, loopt vast
> zodra die tekencombinatie toevallig in de inhoud voorkomt — precies het probleem dat in
> `RHSocketClient` op 10-09-2026 is gerepareerd, toen een leesgrens midden in een groot antwoord
> op een fragment "BYE" viel. Met een lengte vooraan kan dat niet gebeuren, en het scheelt ook
> het base64-coderen van alles wat er over de lijn gaat. `SelfCheck` bevat een controle die
> precies dit randgeval nabootst: een bericht met `**BYE**` en `BYE` in de inhoud, 400 kB groot,
> dat ongeschonden aankomt.
## Eén verbinding, meerdere verzoeken
`RHSocketClient` zet per aanroep een nieuwe verbinding op. Hier blijft één verbinding open:
- verzoeken mogen elkaar overlappen; antwoorden worden teruggekoppeld op `requestId`;
- de server kan uit zichzelf een bericht sturen (een bericht dat binnenkomt, een
overlijdensmelding over jouw profiel) — de app herkent dat aan het ontbreken van een
bijbehorend openstaand verzoek;
- er is niet bij elk verzoek opnieuw een TCP- en TLS-handdruk nodig.
## Verzoek
```json
{
"requestId": "8f2c...",
"action": "vault.list",
"token": "<sessietoken>",
"version": 1,
"device": "Pixel 8",
"payload": { "category": 3 }
}
```
## Antwoord
```json
{
"requestId": "8f2c...",
"status": "Ok",
"message": null,
"payload": [ { "id": 12, "title": "Bank", "category": 3 } ]
}
```
`status` is `"Ok"` of `"Error"` — dezelfde opzet als je RenH-socketserver, zodat het vertrouwd
aanvoelt. Bij `"Error"` staat in `message` een tekst die je zo aan de gebruiker kunt tonen.
## Acties
| Actie | Sessie nodig | Wat het doet |
|---|---|---|
| `ping` | nee | Controle of de server leeft; meldt ook of de verbinding versleuteld is |
| `auth.request-code` | nee | Stuurt een inlogcode naar een bestaand account |
| `auth.signin` | nee | Inloggen met e-mailadres en code; geeft een sessietoken |
| `auth.request-register-code` | nee | Stuurt een code om een account aan te maken; naam gaat mee |
| `auth.register` | nee | Maakt het account aan met de ontvangen code |
| `auth.accept-invitation` | nee | Uitnodiging aannemen en het profiel claimen (geen code nodig) |
| `auth.signout` | ja | Sessie beëindigen |
| `family.members` | ja | Familieleden met hun eigendomsstatus |
| `family.add-member` | ja | Iemand toevoegen |
| `family.invite` | ja | Uitnodiging per e-mail versturen |
| `vault.list` | ja | Titels en categorieën (geen inhoud) |
| `vault.open` | ja | Eén item ontsleutelen |
| `vault.save` | ja | Item versleuteld opslaan |
| `vault.categories` | ja | De soorten kluisitems die jij te zien krijgt: de actieve plus die waar je al een item van hebt, met naam, uitleg, "bespreken met" en bij een eigen soort de velden van het formulier |
| `vault.category` | ja | Eén soort op nummer, ook als hij niet actief is (nodig om het item van een ander te lezen); alleen de beschrijving, nooit inhoud |
| `app.versiehistorie` | ja | De zichtbare regels van de versiehistorie (datum jjjj-mm-dd, versie, kop, tekst), nieuwste dag bovenaan; staat in tabel `version_history`, zie docs/Versiehistorie.md |
| `message.inbox` / `message.outbox` | ja | Berichten voor en door jou |
| `message.read` | ja | Bericht lezen en als gelezen markeren |
| `message.compose` | ja | Bericht klaarzetten |
| `legacy.declare-death` | ja | Overlijden melden |
| `legacy.object` | ja | Een melding over jezelf tegenspreken |
| `legacy.status` | ja | Stand van de procedure |
| `home.advertentie` | ja | De advertentie voor het overzicht, of leeg. Het opvragen telt de vertoning mee |
| `legacy.appoint-manager` | ja | Nalatenschapsbeheerder aanwijzen |
| `legacy.remove-manager` | ja | Een beheerder weer weghalen |
Een actie die een sessie vereist en die zonder geldig token binnenkomt, krijgt altijd:
*Je sessie is verlopen. Log opnieuw in.*, met `"code": "sessie-verlopen"` erbij.
Dat `code`-veld is er voor de gevallen waarin de app iets anders moet doen dan de melding tonen.
Bij `sessie-verlopen` gooit de client het bewaarde token weg en stuurt de schil je naar het
inlogscherm. Op de tekst van de melding sturen zou ook werken, tot iemand er een komma in zet.
De app krijgt `sessie-verlopen` alleen als de server de sessie echt niet meer kent. Een mislukt
verzoek is iets anders: geen bereik, een server die net opstart. Dan blijft het token staan en
probeert de app het de volgende keer gewoon opnieuw. Dat onderscheid is het verschil tussen een
hik en een e-mail met een code.
## Gebruik vanuit de app
`MijnBesluit.Protocol` heeft geen enkel extern package nodig en kan daardoor rechtstreeks in het
MAUI-project worden opgenomen.
```csharp
var verbinding = new MijnBesluitSocketClient("socket.mijnbesluit.nl", 5200, useTls: true);
verbinding.PushReceived += melding =>
Console.WriteLine($"Van de server: {melding.Message}");
// Stap 1: code aanvragen. Die komt per e-mail binnen.
await verbinding.SendAsync(SocketActions.RequestLoginCode, new { email = "jan@example.nl" });
// Stap 2: inloggen met de code die de gebruiker heeft ingetypt.
var inloggen = await verbinding.SendAsync(SocketActions.SignIn,
new { email = "jan@example.nl", code = ingevuldeCode });
if (inloggen.Status == "Ok")
{
verbinding.Token = /* token uit inloggen.Payload */;
var kluis = await verbinding.SendAsync(SocketActions.VaultList);
}
```
Tijdens ontwikkelen mag `AcceptAnyCertificate = true` om met een zelfondertekend certificaat te
werken. Zet dat in een uitgeleverde app nooit aan: dan is TLS zijn waarde kwijt.
## Threads
De server accepteert verbindingen op één thread, geeft elke verbinding een eigen thread met een
blokkerende leeslus, en handelt elk binnengekomen verzoek af op zijn eigen thread. Antwoorden
kunnen daardoor in een andere volgorde terugkomen dan de verzoeken binnenkwamen; daar is
`requestId` voor. Het wegschrijven is geserialiseerd, dus twee antwoorden raken nooit door elkaar.
Zie `docs/SocketServer-installeren.md` voor de instellingen en de grenzen.
## Sessies
Een sessie leeft veertien dagen en schuift op bij elk verzoek. Ze staan in het geheugen én in de
tabel `sessies` (`db/16_sessies.sql`).
Het geheugen is er voor de snelheid: een gewoon verzoek raakt geen database. De tabel is er om
een herstart te overleven. Dat laatste is geen luxe: de app blijft aangemeld en gaat daarna open
met een vingerafdruk of de code van de telefoon. Stonden de sessies alleen in het geheugen, dan
was elke uitrol, elke Windows-update 's nachts en elke reboot genoeg om iedereen weer een code
per e-mail te laten opvragen - het toestel wist dan nog wie je was, de server niet meer. De
database wordt alleen geraadpleegd bij een token dat niet in het geheugen staat, dus in de
praktijk bij het eerste verzoek van elk toestel na een herstart.
In de tabel staat de SHA-256 van het token, nooit het token zelf: uit een hash valt geen token
terug te rekenen. Dezelfde afspraak als bij de inlogcodes.
De vervaltijd schuift op bij gebruik, maar wordt niet bij elk verzoek weggeschreven - zie
`SessionStore.SchuifopDrempel`. Bij een termijn van veertien dagen maakt een uur speling niets
uit, en het scheelt een schrijfactie per aanroep.
Gaat de database niet, dan komt die fout naar buiten als een gewone fout en niet als
"je sessie is verlopen". Ook dat is het verschil tussen een hik en een e-mail met een code.
De tabel `refresh_tokens` uit het oorspronkelijke schema wordt nergens gebruikt; `sessies` is
niet hetzelfde (die heeft de persoon erbij, en er is hier geen apart toegangstoken).
## Poort en certificaat
Standaard poort 5200. In te stellen in `appsettings.json`:
```json
"Socket": {
"Address": "0.0.0.0",
"Port": 5200,
"CertificateFile": "D:\\MijnBesluit\\certificaat.pfx",
"CertificatePassword": "..."
}
```
Of met de vingerafdruk van een certificaat uit de Windows-certificaatopslag
(`CertificateThumbprint`), bijvoorbeeld een certificaat dat je al met Let's Encrypt vernieuwt.