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 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,11 @@ type
|
|||||||
Error: string; // message brut, pour le journal
|
Error: string; // message brut, pour le journal
|
||||||
end;
|
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
|
TSyncClient = class
|
||||||
private
|
private
|
||||||
FBaseUrl: string;
|
FBaseUrl: string;
|
||||||
@@ -64,6 +69,7 @@ type
|
|||||||
/// Attente de la POIGNEE DE MAIN, distincte de celle de la reponse.
|
/// Attente de la POIGNEE DE MAIN, distincte de celle de la reponse.
|
||||||
FConnectMs: Integer;
|
FConnectMs: Integer;
|
||||||
FPrivacyChecked: Boolean;
|
FPrivacyChecked: Boolean;
|
||||||
|
function PublicRepoWarning: string;
|
||||||
public
|
public
|
||||||
constructor Create(const ABaseUrl, AToken: string);
|
constructor Create(const ABaseUrl, AToken: string);
|
||||||
|
|
||||||
@@ -82,11 +88,12 @@ type
|
|||||||
/// l'ecriture (412 = un autre appareil a publie entre-temps).
|
/// l'ecriture (412 = un autre appareil a publie entre-temps).
|
||||||
function IsWebDav: Boolean;
|
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
|
/// UN DEPOT PUBLIC EST REFUSE (Gitea comme GitHub) : chaque synchro est un
|
||||||
/// commit, et l'historique garde tous les instantanes, attaquables hors
|
/// commit, et l'historique garde tous les instantanes, attaquables hors
|
||||||
/// ligne par quiconque peut les lire. Message a afficher, ou '' si le depot
|
/// 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.
|
/// 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
|
/// 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
|
/// aussi bien pour un fichier absent que pour un depot ou un proprietaire
|
||||||
@@ -113,7 +120,7 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
System.JSON, System.NetEncoding;
|
System.JSON, System.NetEncoding, System.StrUtils;
|
||||||
|
|
||||||
function TSyncClient.IsGitea: Boolean;
|
function TSyncClient.IsGitea: Boolean;
|
||||||
begin
|
begin
|
||||||
@@ -207,6 +214,49 @@ begin
|
|||||||
if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := '';
|
if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := '';
|
||||||
end;
|
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<string>;
|
||||||
|
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;
|
function TSyncClient.PublicRepoWarning: string;
|
||||||
var
|
var
|
||||||
LHttp: THTTPClient;
|
LHttp: THTTPClient;
|
||||||
@@ -389,7 +439,7 @@ begin
|
|||||||
Result.Error := 'Ecriture disponible seulement vers Gitea, GitHub ou WebDAV (jeton utilisateur:mot-de-passe)';
|
Result.Error := 'Ecriture disponible seulement vers Gitea, GitHub ou WebDAV (jeton utilisateur:mot-de-passe)';
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
Result.Error := PublicRepoWarning;
|
Result.Error := RefusalReason;
|
||||||
if Result.Error <> '' then
|
if Result.Error <> '' then
|
||||||
begin
|
begin
|
||||||
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
||||||
@@ -522,7 +572,7 @@ begin
|
|||||||
Result.Error := 'URL ou jeton non configure';
|
Result.Error := 'URL ou jeton non configure';
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
Result.Error := PublicRepoWarning;
|
Result.Error := RefusalReason;
|
||||||
if Result.Error <> '' then
|
if Result.Error <> '' then
|
||||||
begin
|
begin
|
||||||
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
||||||
|
|||||||
@@ -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`
|
- **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,
|
(elle en faisait une URL d'API Gitea) — sauf les formes longues d'un dépôt,
|
||||||
`/src/branch/…` et `/blob/…`.
|
`/src/branch/…` et `/blob/…`.
|
||||||
- **Clair autorisé vers `192.168.1.28` seulement** (IP fixe du poste), dans
|
- **`http://` : réseau local seulement**, pour que chacun (les amis compris)
|
||||||
`network_security_config.xml` : le mot de passe WebDAV passe en clair sur le
|
pointe vers son propre serveur sans adresse connue à la compilation. Android
|
||||||
Wi-Fi, le coffre reste chiffré. À retirer quand le serveur parlera HTTPS.
|
ne sait autoriser ni plage ni masque : `network_security_config.xml` permet
|
||||||
Android ne sait pas autoriser « le réseau local » : chaque adresse se nomme.
|
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**
|
- **Côté poste** : pare-feu Windows (règle `WebDAV 8080`, `LocalSubnet`) **et**
|
||||||
Portmaster (entrant autorisé pour Python) — sans le second, le port restait
|
Portmaster (entrant autorisé pour Python) — sans le second, le port restait
|
||||||
fermé malgré la règle.
|
fermé malgré la règle.
|
||||||
|
|||||||
@@ -1,30 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
Le trafic en clair n'est permis que vers la BOUCLE LOCALE, et interdit
|
Le clair est PERMIS ICI, et trie dans le code (PMA.SyncClient.IsLocalHost).
|
||||||
partout ailleurs.
|
|
||||||
|
|
||||||
Il l'etait auparavant partout, via usesCleartextTraffic="true". C'etait pour
|
Chacun pointe vers SON serveur WebDAV, souvent en http:// sur son reseau
|
||||||
une seule raison : l'essai local passe par "adb reverse", qui fait voir le
|
local, a une adresse qu'on ne connait pas a la compilation - et Android ne
|
||||||
poste comme 127.0.0.1 et parle HTTP. Mais l'attribut ne distingue pas la
|
sait autoriser ni une plage ni un masque, seulement des noms un par un.
|
||||||
boucle locale du reste du monde - une URL mal saisie, ou un serveur repassant
|
Avant tout envoi, la synchro refuse donc tout http:// qui ne vise pas le
|
||||||
en HTTP, envoyait le jeton en clair sur le reseau sans que rien ne le signale.
|
reseau local (RFC 1918, boucle, .local, .lan, nom sans point) : vers
|
||||||
|
Internet, le jeton ne part qu'en HTTPS, et le refus s'affiche au lieu de
|
||||||
Ici la permission est nominative. Le jeton ne peut plus sortir qu'en TLS, et
|
laisser passer en silence.
|
||||||
une URL "http://" vers un vrai hote echoue tout de suite, ce qui est
|
|
||||||
exactement le comportement voulu : bruyant, pas silencieux.
|
|
||||||
|
|
||||||
cleartextTrafficPermitted="false" a la racine n'est pas redondant avec le
|
|
||||||
defaut d'Android 9+ : il vaut aussi pour les versions anterieures, et il dit
|
|
||||||
l'intention noir sur blanc.
|
|
||||||
-->
|
-->
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<base-config cleartextTrafficPermitted="false" />
|
<base-config cleartextTrafficPermitted="true" />
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
|
||||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
||||||
<domain includeSubdomains="false">localhost</domain>
|
|
||||||
<!-- ponytail: WebDAV local du poste (IP fixe), en clair sur le Wi-Fi.
|
|
||||||
Le coffre reste chiffre ; le mot de passe WebDAV, lui, passe en
|
|
||||||
clair. A retirer quand le serveur parlera HTTPS. -->
|
|
||||||
<domain includeSubdomains="false">192.168.1.28</domain>
|
|
||||||
</domain-config>
|
|
||||||
</network-security-config>
|
</network-security-config>
|
||||||
|
|||||||
@@ -1757,6 +1757,13 @@ begin
|
|||||||
end
|
end
|
||||||
else if (LUser <> '') then
|
else if (LUser <> '') then
|
||||||
begin
|
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(
|
LHttp.CredentialsStorage.AddCredential(
|
||||||
System.Net.URLClient.TCredentialsStorage.TCredential.Create(
|
System.Net.URLClient.TCredentialsStorage.TCredential.Create(
|
||||||
System.Net.URLClient.TAuthTargetType.Server, '', '', LUser, LPwd));
|
System.Net.URLClient.TAuthTargetType.Server, '', '', LUser, LPwd));
|
||||||
|
|||||||
Reference in New Issue
Block a user