From 0604344c60bda048ab201fa52a8136a8b0b71d11 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:26:17 +0100 Subject: [PATCH] feat(sync): http:// limite au reseau local, Basic des la 1re requete APK : le clair est permis par Android et trie dans TSyncClient.RefusalReason (IsLocalHost : RFC 1918, 127.x, .local, .lan, nom sans point) - chacun pointe vers son propre WebDAV sans IP connue a la compilation ; vers Internet, HTTPS. PC : Authorization Basic envoye d'emblee, plus de 401 anonyme ni de PUT du coffre envoye deux fois. Co-Authored-By: Claude Opus 5.5 --- android-client/PMA.SyncClient.pas | 58 +++++++++++++++++-- android-client/README.md | 13 +++-- .../xml/network_security_config.xml | 33 +++-------- delphi-backend/UMainForm.pas | 7 +++ 4 files changed, 79 insertions(+), 32 deletions(-) diff --git a/android-client/PMA.SyncClient.pas b/android-client/PMA.SyncClient.pas index 8728c88..a8ac530 100644 --- a/android-client/PMA.SyncClient.pas +++ b/android-client/PMA.SyncClient.pas @@ -56,6 +56,11 @@ type Error: string; // message brut, pour le journal end; + /// Vrai pour une adresse du reseau local (RFC 1918, boucle, .local, .lan, + /// nom sans point) : le seul endroit ou le HTTP en clair est accepte. + function IsLocalHost(const AHost: string): Boolean; + +type TSyncClient = class private FBaseUrl: string; @@ -64,6 +69,7 @@ type /// Attente de la POIGNEE DE MAIN, distincte de celle de la reponse. FConnectMs: Integer; FPrivacyChecked: Boolean; + function PublicRepoWarning: string; public constructor Create(const ABaseUrl, AToken: string); @@ -82,11 +88,12 @@ type /// l'ecriture (412 = un autre appareil a publie entre-temps). function IsWebDav: Boolean; + /// Refus LOCAL, avant tout envoi : http:// hors reseau local, puis depot public. /// UN DEPOT PUBLIC EST REFUSE (Gitea comme GitHub) : chaque synchro est un /// commit, et l'historique garde tous les instantanes, attaquables hors /// ligne par quiconque peut les lire. Message a afficher, ou '' si le depot /// est prive ou si on n'a pas pu le savoir. Verifie une fois par instance. - function PublicRepoWarning: string; + function RefusalReason: string; /// POURQUOI UN 404 OU UN 401, en clair pour l'utilisateur. Gitea repond 404 /// aussi bien pour un fichier absent que pour un depot ou un proprietaire @@ -113,7 +120,7 @@ type implementation uses - System.JSON, System.NetEncoding; + System.JSON, System.NetEncoding, System.StrUtils; function TSyncClient.IsGitea: Boolean; begin @@ -207,6 +214,49 @@ begin if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := ''; end; +/// HTTP EN CLAIR : RESEAU LOCAL SEULEMENT. Android le permet partout +/// (network_security_config) pour que chacun pointe vers SON serveur WebDAV, +/// a une adresse qu'on ne connait pas a la compilation ; le tri se fait donc +/// ici. Vers Internet, le jeton ne part qu'en HTTPS. +/// ponytail: un Wi-Fi etranger qui reutilise la meme plage recevrait le jeton +/// en clair - le coffre, lui, reste chiffre. HTTPS chez soi supprime le risque. +function IsLocalHost(const AHost: string): Boolean; +var + LParts: TArray; + A, B: Integer; +begin + var LHost := AHost.ToLower; + if (LHost = 'localhost') or LHost.EndsWith('.local') or LHost.EndsWith('.lan') or + (LHost.IndexOf('.') < 0) and (LHost <> '') then + Exit(True); + LParts := LHost.Split(['.']); + if (Length(LParts) <> 4) or not TryStrToInt(LParts[0], A) or + not TryStrToInt(LParts[1], B) then + Exit(False); + for var LPart in LParts do + if not TryStrToInt(LPart, A) then Exit(False); + A := StrToInt(LParts[0]); + Result := (A = 10) or (A = 127) or ((A = 192) and (B = 168)) or + ((A = 172) and (B >= 16) and (B <= 31)); +end; + +function TSyncClient.RefusalReason: string; +var + LHost: string; +begin + if StartsText('http://', FBaseUrl) then + begin + try + LHost := TURI.Create(FBaseUrl).Host; + except + LHost := ''; + end; + if not IsLocalHost(LHost) then + Exit('http:// is only allowed on the local network (192.168.x.x, 10.x.x.x, .local, .lan) - use https:// for ' + LHost); + end; + Result := PublicRepoWarning; +end; + function TSyncClient.PublicRepoWarning: string; var LHttp: THTTPClient; @@ -389,7 +439,7 @@ begin Result.Error := 'Ecriture disponible seulement vers Gitea, GitHub ou WebDAV (jeton utilisateur:mot-de-passe)'; Exit; end; - Result.Error := PublicRepoWarning; + Result.Error := RefusalReason; if Result.Error <> '' then begin Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur @@ -522,7 +572,7 @@ begin Result.Error := 'URL ou jeton non configure'; Exit; end; - Result.Error := PublicRepoWarning; + Result.Error := RefusalReason; if Result.Error <> '' then begin Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur diff --git a/android-client/README.md b/android-client/README.md index 9e27c31..fdf58f1 100644 --- a/android-client/README.md +++ b/android-client/README.md @@ -434,10 +434,15 @@ l'ouverture, écriture 204, puis synchro du poste qui reprend la modification. - **Une URL qui finit en `.json` n'est plus réécrite** par `normalizeRepoUrl` (elle en faisait une URL d'API Gitea) — sauf les formes longues d'un dépôt, `/src/branch/…` et `/blob/…`. -- **Clair autorisé vers `192.168.1.28` seulement** (IP fixe du poste), dans - `network_security_config.xml` : le mot de passe WebDAV passe en clair sur le - Wi-Fi, le coffre reste chiffré. À retirer quand le serveur parlera HTTPS. - Android ne sait pas autoriser « le réseau local » : chaque adresse se nomme. +- **`http://` : réseau local seulement**, pour que chacun (les amis compris) + pointe vers son propre serveur sans adresse connue à la compilation. Android + ne sait autoriser ni plage ni masque : `network_security_config.xml` permet + donc le clair partout, et `TSyncClient.RefusalReason` refuse **avant tout + envoi** un `http://` dont l'hôte n'est pas local (`IsLocalHost` : RFC 1918, + `127.x`, `.local`, `.lan`, nom sans point). Vers Internet, HTTPS obligatoire, + et le refus s'affiche dans les réglages. Risque assumé : un Wi-Fi étranger + qui réutilise la même plage recevrait le mot de passe WebDAV en clair — le + coffre, lui, reste chiffré. - **Côté poste** : pare-feu Windows (règle `WebDAV 8080`, `LocalSubnet`) **et** Portmaster (entrant autorisé pour Python) — sans le second, le port restait fermé malgré la règle. diff --git a/android-client/android-res/xml/network_security_config.xml b/android-client/android-res/xml/network_security_config.xml index d0fd800..e1cbcd0 100644 --- a/android-client/android-res/xml/network_security_config.xml +++ b/android-client/android-res/xml/network_security_config.xml @@ -1,30 +1,15 @@ - - - 127.0.0.1 - localhost - - 192.168.1.28 - + diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index e429489..eae9da9 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -1757,6 +1757,13 @@ begin end else if (LUser <> '') then begin + // BASIC DES LA PREMIERE REQUETE, comme le telephone. Laisse a + // Windows, l'authentification partait d'abord en anonyme : 401, + // puis la meme requete - et un PUT envoyait le coffre deux fois. + // Les identifiants restent aussi en reserve pour un serveur Digest. + LHttp.CustomHeaders['Authorization'] := 'Basic ' + + TNetEncoding.Base64.Encode(LUser + ':' + LPwd) + .Replace(#13, '').Replace(#10, ''); LHttp.CredentialsStorage.AddCredential( System.Net.URLClient.TCredentialsStorage.TCredential.Create( System.Net.URLClient.TAuthTargetType.Server, '', '', LUser, LPwd));