Files
Password-Manager/android-client/PMA.SyncClient.pas
T
r-zakarya 8796bb341d feat(android): HTTP client for the sync endpoint, with ETag reuse
Fetches the opaque container and the ETag; a 304 skips both the download and the 4-second derivation, which is the whole point of keeping the tag between calls. Redirects are not followed - one to another host would send the bearer token there.

Local testing goes through adb reverse tcp:8080 tcp:80, which makes the phone reach the PC as 127.0.0.1 and satisfies the endpoint's loopback exemption. That needs usesCleartextTraffic in the manifest, marked for removal before shipping.
2026-08-14 23:37:06 +01:00

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.