diff --git a/AndroidManifest.template.xml b/AndroidManifest.template.xml
index 6be2ed1..2e31e2c 100644
--- a/AndroidManifest.template.xml
+++ b/AndroidManifest.template.xml
@@ -19,12 +19,13 @@
<%queries-child-elements%>
-
+
` et non
+`Bearer` (l'un à la place de l'autre donne un 401 muet), le corps est du JSON
+dont il faut extraire `content` en base64, et **la comparaison d'empreinte se
+fait dans le client** — l'API ne connaît pas `If-None-Match`, donc le corps est
+retéléchargé, mais la dérivation de clé, elle, est bien épargnée. Le champ
+`ETag` du résultat porte le `sha` : l'appelant n'a pas à savoir lequel des deux
+transports lui répond.
+
+**Rien de tout cela n'a encore atteint un vrai dépôt.** Il faut, dans cet ordre :
+un dépôt privé, un fichier `vault-sync.json` publié par le poste Windows, un
+jeton limité à ce dépôt, puis l'URL de contenu complète dans *Server settings*.
+Tant que le poste publie sur `sync.php`, le téléphone continue de lire là.
+
+**Reste à faire** : le PUT (§4 seulement — la lecture n'écrit rien), la même
+bascule côté poste Windows en remplacement de WebDAV, et une purge périodique de
+l'historique — sans quoi le dépôt grossit
d'un instantané complet à chaque synchronisation, pièces jointes comprises.
## Ordre proposé
diff --git a/android-client/PMA.SyncClient.pas b/android-client/PMA.SyncClient.pas
index f9d9c30..088db25 100644
--- a/android-client/PMA.SyncClient.pas
+++ b/android-client/PMA.SyncClient.pas
@@ -1,15 +1,29 @@
unit PMA.SyncClient;
{
- Lecture de l'instantane chiffre depuis l'endpoint PHP (server-php/sync.php).
+ Lecture de l'instantane chiffre. DEUX TRANSPORTS, reconnus a l'URL :
- 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.
+ - un depot Gitea prive, via l'API des contenus. C'est la destination : plus
+ rien a heberger, HTTPS et certificat deja en place, acces depuis
+ l'exterieur gratuit.
+ - l'endpoint PHP historique (server-php/sync.php), garde le temps que le
+ poste Windows publie aussi dans le depot. Sans lui, la migration exigerait
+ de tout basculer d'un coup, des deux cotes, sans retour possible.
- 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.
+ Ne dechiffre rien : il ramene des octets opaques et l'empreinte qui les
+ identifie. Le dechiffrement est le travail de PMA.Crypto, et il coute
+ 4 secondes - d'ou l'importance de cette empreinte, qui evite de le refaire
+ quand rien n'a change.
+
+ CE QUE L'EMPREINTE DESIGNE DIFFERE selon le transport, mais son role est le
+ meme et l'appelant n'a pas a le savoir : l'ETag HTTP pour PHP, le sha du blob
+ pour Gitea. Sur Gitea la comparaison se fait ICI, apres reception : l'API ne
+ connait pas If-None-Match, donc le corps est retelecharge - ce qui est
+ bon marche - mais la derivation, elle, est bien evitee.
+
+ Le PHP 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. Gitea repond de meme
+ sur un depot prive. Le message utilisateur doit couvrir les deux cas.
}
interface
@@ -31,7 +45,9 @@ type
TSyncResult = record
Status: TSyncStatus;
Container: string; // le JSON chiffre, vide sauf sur ssOK
- ETag: string; // a repasser au prochain appel
+ /// Ce qui identifie ce contenu : ETag HTTP sur PHP, sha du blob sur Gitea.
+ /// A ranger avec le clair et a repasser au prochain appel.
+ ETag: string;
HttpCode: Integer;
Error: string; // message brut, pour le journal
end;
@@ -44,10 +60,15 @@ type
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.
+ /// GET de l'instantane. AKnownETag non vide -> ssNotModified si le serveur
+ /// rend le meme contenu, ce qui epargne la derivation de la cle.
function Fetch(const AKnownETag: string = ''): TSyncResult;
+ /// Vrai si l'URL designe l'API des contenus de Gitea. C'est le seul
+ /// discriminant : le chemin de cette API est fixe, et une URL de dépôt qui
+ /// n'y ressemble pas est de toute facon inutilisable.
+ function IsGitea: Boolean;
+
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
@@ -57,6 +78,65 @@ type
implementation
+uses
+ System.JSON, System.NetEncoding;
+
+function TSyncClient.IsGitea: Boolean;
+begin
+ Result := FBaseUrl.ToLower.Contains('/api/v1/repos/');
+end;
+
+/// Extrait le conteneur et le sha de la reponse de l'API des contenus. Rend
+/// faux, avec la raison, si la reponse n'a pas la forme attendue - une URL de
+/// dépôt juste a cote rend du JSON, mais pas celui-la.
+function ParseGiteaContents(const ABody: string;
+ out AContainer, ASha, AError: string): Boolean;
+var
+ LRoot: TJSONObject;
+ LEncoded, LEncoding: string;
+begin
+ Result := False;
+ AContainer := '';
+ ASha := '';
+ LRoot := TJSONObject.ParseJSONValue(ABody) as TJSONObject;
+ if LRoot = nil then
+ begin
+ AError := 'reponse Gitea illisible (pas du JSON)';
+ Exit;
+ end;
+ try
+ ASha := LRoot.GetValue('sha', '');
+ LEncoding := LRoot.GetValue('encoding', '');
+ LEncoded := LRoot.GetValue('content', '');
+ if (LEncoded = '') or (ASha = '') then
+ begin
+ AError := 'reponse Gitea sans content/sha - URL de contenu correcte ?';
+ Exit;
+ end;
+ if not SameText(LEncoding, 'base64') then
+ begin
+ AError := 'encodage Gitea inattendu : ' + LEncoding;
+ Exit;
+ end;
+ // Le base64 de Gitea est decoupe en lignes ; le decodeur s'en accommode,
+ // mais on ne compte pas dessus.
+ LEncoded := LEncoded.Replace(#13, '').Replace(#10, '');
+ try
+ AContainer := TEncoding.UTF8.GetString(TNetEncoding.Base64.DecodeStringToBytes(LEncoded));
+ except
+ on E: Exception do
+ begin
+ AError := 'base64 Gitea invalide : ' + E.Message;
+ Exit;
+ end;
+ end;
+ Result := AContainer.Trim <> '';
+ if not Result then AError := 'fichier vide dans le depot';
+ finally
+ LRoot.Free;
+ end;
+end;
+
constructor TSyncClient.Create(const ABaseUrl, AToken: string);
begin
inherited Create;
@@ -87,9 +167,19 @@ begin
// 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;
+ // Gitea attend "token " pour un jeton d'acces personnel ; le PHP
+ // attend "Bearer". Envoyer l'un a la place de l'autre donne un 401 qui ne
+ // dit pas pourquoi.
+ if IsGitea then
+ LHttp.CustomHeaders['Authorization'] := 'token ' + FToken
+ else
+ begin
+ LHttp.CustomHeaders['Authorization'] := 'Bearer ' + FToken;
+ // Pas sur Gitea : le sha n'est pas un ETag HTTP, et l'envoyer ferait
+ // repondre 200 avec un corps complet de toute facon.
+ if AKnownETag <> '' then
+ LHttp.CustomHeaders['If-None-Match'] := AKnownETag;
+ end;
LStream := TStringStream.Create('', TEncoding.UTF8);
try
@@ -109,6 +199,23 @@ begin
case LResp.StatusCode of
200:
+ if IsGitea then
+ begin
+ if not ParseGiteaContents(LStream.DataString, Result.Container,
+ Result.ETag, Result.Error) then
+ Result.Status := ssHttpError
+ // Meme sha : le contenu est celui qu'on detient deja. On rend la
+ // meme reponse qu'un 304 pour que l'appelant epargne la derivation,
+ // et on jette le conteneur - il ne servirait a rien.
+ else if (AKnownETag <> '') and (Result.ETag = AKnownETag) then
+ begin
+ Result.Status := ssNotModified;
+ Result.Container := '';
+ end
+ else
+ Result.Status := ssOK;
+ end
+ else
begin
Result.Status := ssOK;
Result.Container := LStream.DataString;
@@ -127,7 +234,14 @@ begin
Result.Status := ssRefused;
Result.Error := 'Le serveur exige HTTPS';
end;
+ // Gitea repond 404 sur un depot prive qu'on n'a pas le droit de voir,
+ // comme sur un fichier absent - exactement l'ambiguite du PHP.
404: Result.Status := ssNoSnapshot;
+ 401:
+ begin
+ Result.Status := ssHttpError;
+ Result.Error := '401 : jeton refuse';
+ end;
else
Result.Status := ssHttpError;
Result.Error := 'HTTP ' + LResp.StatusCode.ToString + ' ' + LResp.StatusText;
diff --git a/android-client/android-res/xml/network_security_config.xml b/android-client/android-res/xml/network_security_config.xml
new file mode 100644
index 0000000..95af0d5
--- /dev/null
+++ b/android-client/android-res/xml/network_security_config.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ 127.0.0.1
+ localhost
+
+
diff --git a/android-client/build-apk.ps1 b/android-client/build-apk.ps1
index 10167d6..d54dd3e 100644
--- a/android-client/build-apk.ps1
+++ b/android-client/build-apk.ps1
@@ -39,8 +39,19 @@ Copy-Item "$out\AndroidManifest.xml" "$stage\AndroidManifest.xml" -Force
Copy-Item "$out\pmserverApk.classes\*.dex" "$stage\classes\" -Force
& $strip --strip-debug -o "$stage\library\lib\arm64-v8a\libpmserverApk.so" "$out\libpmserverApk.so"
-cmd /c "$msbuild /p:Platform=Android64 /p:Config=Debug /t:Deploy /v:minimal /nologo" 2>&1 |
- Select-String -Pattern 'error' | ForEach-Object { $_.Line }
+# L'AAPT DE DEPLOY ECHOUE, ET C'EST ATTENDU. Le manifeste reference
+# @xml/network_security_config, que la generation de ressources de Delphi ne
+# contient pas ; poser le fichier avant Deploy ne sert a rien, Deploy reconstruit
+# res\ juste avant d'appeler aapt. L'etape suivante repose nos ressources et
+# refait l'archive entiere, donc l'APK livre est complet - verifie a
+# "aapt list" : res/xml/network_security_config.xml y est, et l'attribut du
+# manifeste resout vers un identifiant reel.
+#
+# On ne masque donc que CETTE erreur, nommement, et tout autre message remonte.
+$deploy = cmd /c "$msbuild /p:Platform=Android64 /p:Config=Debug /t:Deploy /v:minimal /nologo" 2>&1
+$deploy | Select-String -Pattern 'error' |
+ Where-Object { $_.Line -notmatch 'network_security_config|Unable to execute.*Aapt\.exe' } |
+ ForEach-Object { $_.Line }
# 4. aapt + zipalign + apksigner, et remise en place de ce que Deploy efface.
& "$PSScriptRoot\repack-apk.ps1" | Select-Object -Last 1