diff --git a/AndroidManifest.template.xml b/AndroidManifest.template.xml
index b19eb87..e295708 100644
--- a/AndroidManifest.template.xml
+++ b/AndroidManifest.template.xml
@@ -12,7 +12,14 @@
<%queries-child-elements%>
+
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.
diff --git a/mainApk.pas b/mainApk.pas
index 1cf1c83..bc9af20 100644
--- a/mainApk.pas
+++ b/mainApk.pas
@@ -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('entries', LArr) then
+ begin
+ LLive := 0; LTrash := 0;
+ for I := 0 to LArr.Count - 1 do
+ if (LArr.Items[I] as TJSONObject).GetValue('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;
diff --git a/pmserverApk.dpr b/pmserverApk.dpr
index b91a8bd..09f544e 100644
--- a/pmserverApk.dpr
+++ b/pmserverApk.dpr
@@ -5,7 +5,8 @@ uses
FMX.Forms,
mainApk in 'mainApk.pas' {Form8},
PMA.Crypto in 'android-client\PMA.Crypto.pas',
- PMA.TestVector in 'android-client\PMA.TestVector.pas';
+ PMA.TestVector in 'android-client\PMA.TestVector.pas',
+ PMA.SyncClient in 'android-client\PMA.SyncClient.pas';
{$R *.res}