177 lines
7.6 KiB
Markdown
177 lines
7.6 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 |
|
|
| `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.
|