unit PMA.SyncClient; { Lecture de l'instantane chiffre depuis l'endpoint PHP (server-php/sync.php). Ne dechiffre rien : il ramene des octets opaques et l'ETag qui les identifie. Le dechiffrement est le travail de PMA.Crypto, et il coute 4 secondes — d'ou l'importance de l'ETag, qui evite de le refaire quand rien n'a change. Le serveur repond 404 aussi bien pour un jeton invalide que pour un instantane absent : c'est voulu (un appelant non authentifie n'apprend pas ce qui existe), et le client ne peut donc pas les distinguer. Le message utilisateur doit couvrir les deux cas. } interface uses System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.URLClient, System.Net.HttpClientComponent; type TSyncStatus = ( ssOK, // 200 : nouveau contenu dans Container ssNotModified, // 304 : l'ETag detenu est toujours le bon ssNoSnapshot, // 404 : pas d'instantane, OU jeton invalide ssRefused, // 403 : HTTPS exige par le serveur ssHttpError, // autre code ssNetworkError // rien n'a repondu ); TSyncResult = record Status: TSyncStatus; Container: string; // le JSON chiffre, vide sauf sur ssOK ETag: string; // a repasser au prochain appel HttpCode: Integer; Error: string; // message brut, pour le journal end; TSyncClient = class private FBaseUrl: string; FToken: string; FTimeoutMs: Integer; public constructor Create(const ABaseUrl, AToken: string); /// GET de l'instantane. AKnownETag non vide -> If-None-Match, et un 304 /// evite de retelecharger ET de re-deriver la cle. function Fetch(const AKnownETag: string = ''): TSyncResult; property BaseUrl: string read FBaseUrl write FBaseUrl; property Token: string read FToken write FToken; /// Volontairement court : sur mobile, un reseau qui ne repond pas doit /// rendre la main vite plutot que de figer l'ecran. property TimeoutMs: Integer read FTimeoutMs write FTimeoutMs; end; implementation constructor TSyncClient.Create(const ABaseUrl, AToken: string); begin inherited Create; FBaseUrl := ABaseUrl.Trim; FToken := AToken.Trim; FTimeoutMs := 15000; end; function TSyncClient.Fetch(const AKnownETag: string): TSyncResult; var LHttp: THTTPClient; LResp: IHTTPResponse; LStream: TStringStream; begin Result := Default(TSyncResult); Result.Status := ssNetworkError; if (FBaseUrl = '') or (FToken = '') then begin Result.Error := 'URL ou jeton non configure'; Exit; end; LHttp := THTTPClient.Create; try LHttp.ConnectionTimeout := FTimeoutMs; LHttp.ResponseTimeout := FTimeoutMs; // Pas de redirection suivie : une redirection vers un autre hote nous // ferait envoyer le jeton ailleurs. LHttp.HandleRedirects := False; LHttp.CustomHeaders['Authorization'] := 'Bearer ' + FToken; if AKnownETag <> '' then LHttp.CustomHeaders['If-None-Match'] := AKnownETag; LStream := TStringStream.Create('', TEncoding.UTF8); try try LResp := LHttp.Get(FBaseUrl, LStream); except on E: Exception do begin Result.Status := ssNetworkError; Result.Error := E.ClassName + ': ' + E.Message; Exit; end; end; Result.HttpCode := LResp.StatusCode; Result.ETag := LResp.HeaderValue['ETag']; case LResp.StatusCode of 200: begin Result.Status := ssOK; Result.Container := LStream.DataString; // Un corps vide avec un 200 signale un proxy ou une erreur de // configuration, pas un instantane : le signaler ici evite un // "mot de passe incorrect" trompeur plus loin. if Result.Container.Trim = '' then begin Result.Status := ssHttpError; Result.Error := '200 avec un corps vide'; end; end; 304: Result.Status := ssNotModified; 403: begin Result.Status := ssRefused; Result.Error := 'Le serveur exige HTTPS'; end; 404: Result.Status := ssNoSnapshot; else Result.Status := ssHttpError; Result.Error := 'HTTP ' + LResp.StatusCode.ToString + ' ' + LResp.StatusText; end; finally LStream.Free; end; finally LHttp.Free; end; end; end.