feat(sync): GitHub en plus de Gitea, et refus d'un depot public

GitHub parle la meme API des contenus (base64, sha, corps du PUT) : meme
chemin, reconnu a api.github.com/repos/. User-Agent pose partout (GitHub
refuse sans). Un depot public est refuse avant toute lecture ou ecriture,
Gitea comme GitHub : l'historique garde tous les instantanes chiffres.
Telephone : github.com/O/R se convertit en URL d'API et inversement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-18 10:36:18 +01:00
parent d7a8b4ddff
commit 3c16b41714
5 changed files with 136 additions and 14 deletions
+78 -6
View File
@@ -61,6 +61,7 @@ type
FBaseUrl: string;
FToken: string;
FTimeoutMs: Integer;
FPrivacyChecked: Boolean;
public
constructor Create(const ABaseUrl, AToken: string);
@@ -73,6 +74,12 @@ type
/// n'y ressemble pas est de toute facon inutilisable.
function IsGitea: Boolean;
/// 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;
/// 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
/// inexistant : deux requetes de plus tranchent - le depot, puis
@@ -102,7 +109,55 @@ uses
function TSyncClient.IsGitea: Boolean;
begin
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/');
// GitHub parle la meme API des contenus : meme chemin pour les deux.
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/') or
FBaseUrl.ToLower.Contains('api.github.com/repos/');
end;
/// URL de l'API du depot (sans /contents/...), identique pour Gitea
/// (https://hote/api/v1/repos/O/R) et GitHub (https://api.github.com/repos/O/R).
function RepoApiUrl(const AContentsUrl: string): string;
var
LP: Integer;
begin
LP := Pos('/contents/', AContentsUrl);
if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := '';
end;
function TSyncClient.PublicRepoWarning: string;
var
LHttp: THTTPClient;
LResp: IHTTPResponse;
LDoc: TJSONObject;
LPriv: Boolean;
begin
Result := '';
if FPrivacyChecked or not IsGitea or (RepoApiUrl(FBaseUrl) = '') then Exit;
FPrivacyChecked := True;
LHttp := THTTPClient.Create;
try
LHttp.ConnectionTimeout := FTimeoutMs;
LHttp.ResponseTimeout := FTimeoutMs;
LHttp.HandleRedirects := False;
LHttp.UserAgent := 'PMServer';
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
try
LResp := LHttp.Get(RepoApiUrl(FBaseUrl));
except
Exit; // doute reseau : pas de verdict, la vraie requete le dira
end;
if LResp.StatusCode <> 200 then Exit;
LDoc := TJSONObject.ParseJSONValue(LResp.ContentAsString(TEncoding.UTF8)) as TJSONObject;
if LDoc = nil then Exit;
try
if LDoc.TryGetValue<Boolean>('private', LPriv) and not LPriv then
Result := 'This repository is PUBLIC - make it private before syncing: anyone could download your encrypted vault and all its history.';
finally
LDoc.Free;
end;
finally
LHttp.Free;
end;
end;
function TSyncClient.Diagnose(AHttpCode: Integer): string;
@@ -126,10 +181,12 @@ begin
if AHttpCode = 401 then
Exit('Token refused - expired, revoked or mistyped');
if (AHttpCode <> 404) or not IsGitea then Exit;
// https://hote/api/v1/repos/OWNER/REPO/contents/FICHIER
LP := Pos('/api/v1/repos/', LowerCase(FBaseUrl));
LApi := Copy(FBaseUrl, 1, LP + Length('/api/v1/') - 1);
LParts := Copy(FBaseUrl, LP + Length('/api/v1/repos/'), MaxInt).Split(['/']);
// https://hote/api/v1/repos/O/R/contents/F ou https://api.github.com/repos/O/R/...
// Tout ce qui precede "repos/" est la racine de l'API, pour les deux.
LP := Pos('/repos/', LowerCase(FBaseUrl));
if LP = 0 then Exit;
LApi := Copy(FBaseUrl, 1, LP);
LParts := Copy(FBaseUrl, LP + Length('/repos/'), MaxInt).Split(['/']);
if Length(LParts) < 2 then Exit;
LOwner := LParts[0];
LRepo := LParts[1];
@@ -138,6 +195,7 @@ begin
LHttp.ConnectionTimeout := FTimeoutMs;
LHttp.ResponseTimeout := FTimeoutMs;
LHttp.HandleRedirects := False;
LHttp.UserAgent := 'PMServer';
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
case CodeOf(LApi + 'repos/' + LOwner + '/' + LRepo) of
200: Result := 'No vault in this repository yet - sync from the PC first';
@@ -237,7 +295,7 @@ begin
if not IsGitea then
begin
Result.Status := ssHttpError;
Result.Error := 'Ecriture disponible seulement vers un depot Gitea';
Result.Error := 'Ecriture disponible seulement vers un depot Gitea ou GitHub';
Exit;
end;
if (FBaseUrl = '') or (FToken = '') then
@@ -245,6 +303,12 @@ begin
Result.Error := 'URL ou jeton non configure';
Exit;
end;
Result.Error := PublicRepoWarning;
if Result.Error <> '' then
begin
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
Exit;
end;
LEnv := TJSONObject.Create;
LB64 := TBase64Encoding.Create(0); // pas de coupure : voir PMA.Crypto
@@ -266,6 +330,7 @@ begin
LHttp.ConnectionTimeout := FTimeoutMs;
LHttp.ResponseTimeout := FTimeoutMs;
LHttp.HandleRedirects := False;
LHttp.UserAgent := 'PMServer'; // GitHub refuse toute requete sans
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
LHttp.CustomHeaders['Content-Type'] := 'application/json';
try
@@ -346,11 +411,18 @@ begin
Result.Error := 'URL ou jeton non configure';
Exit;
end;
Result.Error := PublicRepoWarning;
if Result.Error <> '' then
begin
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
Exit;
end;
LHttp := THTTPClient.Create;
try
LHttp.ConnectionTimeout := FTimeoutMs;
LHttp.ResponseTimeout := FTimeoutMs;
LHttp.UserAgent := 'PMServer'; // GitHub refuse toute requete sans
// Pas de redirection suivie : une redirection vers un autre hote nous
// ferait envoyer le jeton ailleurs.
LHttp.HandleRedirects := False;