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.
This commit is contained in:
r-zakarya
2026-08-14 23:37:06 +01:00
parent c684d9073f
commit 8796bb341d
4 changed files with 279 additions and 5 deletions
+127 -4
View File
@@ -21,14 +21,18 @@ uses
System.Hash, System.Diagnostics, System.NetEncoding,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
FMX.ScrollBox, FMX.Layouts;
FMX.ScrollBox, FMX.Layouts, FMX.Edit;
type
TForm8 = class(TForm)
private
FLog: TMemo;
FBtn: TButton;
FBtnFetch: TButton;
FUrl, FToken, FPwd: TEdit;
FETag: string; // conserve entre deux appuis : c'est ce qui permet le 304
procedure RunSelfTest(Sender: TObject);
procedure RunFetch(Sender: TObject);
procedure Log(const AMsg: string);
public
constructor Create(AOwner: TComponent); override;
@@ -40,7 +44,7 @@ var
implementation
uses
PMA.Crypto, PMA.TestVector;
System.JSON, PMA.Crypto, PMA.TestVector, PMA.SyncClient;
{$R *.fmx}
@@ -49,12 +53,38 @@ begin
inherited;
Caption := 'PM — crypto self-test';
// Champs de configuration. Rien n'est persiste : c'est un banc d'essai, et
// le mot de passe de sync ouvre le coffre entier — il n'a pas a trainer sur
// le disque tant que le stockage Keystore n'est pas ecrit.
FUrl := TEdit.Create(Self);
FUrl.Parent := Self; FUrl.Align := TAlignLayout.Top;
FUrl.Margins.Rect := RectF(8, 8, 8, 0);
FUrl.TextPrompt := 'http://127.0.0.1:8080/server-php/sync.php';
FToken := TEdit.Create(Self);
FToken.Parent := Self; FToken.Align := TAlignLayout.Top;
FToken.Margins.Rect := RectF(8, 4, 8, 0);
FToken.TextPrompt := 'jeton Bearer';
FPwd := TEdit.Create(Self);
FPwd.Parent := Self; FPwd.Align := TAlignLayout.Top;
FPwd.Margins.Rect := RectF(8, 4, 8, 0);
FPwd.TextPrompt := 'mot de passe de sync';
FPwd.Password := True;
FBtnFetch := TButton.Create(Self);
FBtnFetch.Parent := Self; FBtnFetch.Align := TAlignLayout.Top;
FBtnFetch.Margins.Rect := RectF(8, 4, 8, 0);
FBtnFetch.Height := 48;
FBtnFetch.Text := 'Telecharger et dechiffrer';
FBtnFetch.OnClick := RunFetch;
FBtn := TButton.Create(Self);
FBtn.Parent := Self;
FBtn.Align := TAlignLayout.Top;
FBtn.Margins.Rect := RectF(8, 8, 8, 8);
FBtn.Margins.Rect := RectF(8, 4, 8, 8);
FBtn.Height := 48;
FBtn.Text := 'Lancer le test';
FBtn.Text := 'Test crypto (vecteur)';
FBtn.OnClick := RunSelfTest;
FLog := TMemo.Create(Self);
@@ -77,6 +107,99 @@ begin
end);
end;
procedure TForm8.RunFetch(Sender: TObject);
var
LUrl, LTok, LPwd: string;
begin
FLog.Lines.Clear;
FBtnFetch.Enabled := False;
// Lus ICI, sur le thread principal : toucher un controle FMX depuis le
// thread de travail est un plantage differe, pas une erreur de compilation.
LUrl := FUrl.Text.Trim;
LTok := FToken.Text.Trim;
LPwd := FPwd.Text;
TThread.CreateAnonymousThread(
procedure
var
LClient: TSyncClient;
LRes: TSyncResult;
LPlain: string;
LSw: TStopwatch;
LJson: TJSONObject;
LArr: TJSONArray;
LLive, LTrash, I: Integer;
begin
LClient := TSyncClient.Create(LUrl, LTok);
try
Log('GET ' + LUrl);
if FETag <> '' then Log(' If-None-Match: ' + FETag);
LSw := TStopwatch.StartNew;
LRes := LClient.Fetch(FETag);
LSw.Stop;
Log(' HTTP ' + LRes.HttpCode.ToString + ' en ' +
LSw.ElapsedMilliseconds.ToString + ' ms');
case LRes.Status of
ssNotModified:
begin
Log(' ==> 304 : inchange, rien a re-dechiffrer');
Exit;
end;
ssNoSnapshot:
begin
// Le serveur repond pareil dans les deux cas, par conception.
Log(' ==> 404 : pas d''instantane OU jeton invalide');
Exit;
end;
ssRefused, ssHttpError, ssNetworkError:
begin
Log(' ==> echec : ' + LRes.Error);
Exit;
end;
end;
Log(' recu ' + Length(LRes.Container).ToString + ' octets');
if LRes.ETag <> '' then
begin
FETag := LRes.ETag;
Log(' ETag conserve : ' + FETag);
end;
Log('');
Log('Dechiffrement...');
LSw := TStopwatch.StartNew;
LPlain := DecryptContainer(LRes.Container, LPwd);
LSw.Stop;
Log(' ' + LSw.ElapsedMilliseconds.ToString + ' ms pour ' +
Length(LPlain).ToString + ' caracteres');
// Compte vivant / corbeille : c'est ce que le vrai client devra
// afficher separement.
LJson := TJSONObject.ParseJSONValue(LPlain) as TJSONObject;
if LJson <> nil then
try
if LJson.TryGetValue<TJSONArray>('entries', LArr) then
begin
LLive := 0; LTrash := 0;
for I := 0 to LArr.Count - 1 do
if (LArr.Items[I] as TJSONObject).GetValue<Integer>('deleted', 0) = 1
then Inc(LTrash) else Inc(LLive);
Log(' ' + LLive.ToString + ' entrees, ' +
LTrash.ToString + ' en corbeille');
end;
finally
LJson.Free;
end;
except
on E: Exception do
Log('ERREUR ' + E.ClassName + ' : ' + E.Message);
end;
LClient.Free;
TThread.Queue(nil, procedure begin FBtnFetch.Enabled := True; end);
end).Start;
end;
procedure TForm8.RunSelfTest(Sender: TObject);
begin
FLog.Lines.Clear;