Eerste commit
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user