# 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": "", "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.