5fdb7f914e
Opening a container costs ~4 s and the iteration count belongs to the desktop format, so the fix is to stop repeating it: derive once per snapshot, then read a local AES file which the hardware accelerates. The key is generated inside the Keystore and never leaves it - the file alone is useless, even rooted. ETag and vault are encrypted together on purpose: an ETag outliving an unreadable vault would make the server answer 304 with nothing to show. A failed read wipes the cache rather than guessing. Also normalises every Delphi source to pure ASCII: Delphi reads a BOM-less file as ANSI, so accented comments came back as mojibake, and each PowerShell rewrite added another layer.
144 lines
4.3 KiB
ObjectPascal
144 lines
4.3 KiB
ObjectPascal
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.
|