feat(server): un port occupe ne tue plus le demarrage
Le lien n'etait garde par rien : un port pris levait, et l'application s'ouvrait sur une boite d'erreur. Trois essais desormais - le premier honore le port de config.txt, que l'utilisateur a peut-etre choisi expres, les suivants prennent un ephemere. SE LIER EST LE TEST. Sonder le port avant serait plus faible - un service qui ecoute sans repondre passerait inapercu - et sujet a une course : le port peut etre pris entre le sondage et le lien. Chaque deplacement est journalise, un serveur qui change de port en silence etant pire qu'un serveur qui refuse de demarrer. Trouve en chemin : Randomize n'est appele NULLE PART, donc le port "aleatoire" de port=0 etait le meme a chaque lancement - l'aleatoire cense compliquer un balayage exterieur n'existait pas. Il vient maintenant de RandomBytes, donc de BCryptGenRandom, deja utilise pour le jeton d'acces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -250,6 +250,7 @@ Détails :
|
|||||||
Fichier `config.txt` à côté de l'exe (tous champs optionnels) :
|
Fichier `config.txt` à côté de l'exe (tous champs optionnels) :
|
||||||
```
|
```
|
||||||
port=8765 # 0 ou absent = port aléatoire ephemeral (49152-65535)
|
port=8765 # 0 ou absent = port aléatoire ephemeral (49152-65535)
|
||||||
|
# occupé = jusqu'à 3 essais, replis en éphémère
|
||||||
debug=true # affiche PanelTop (Start/Stop, log memo)
|
debug=true # affiche PanelTop (Start/Stop, log memo)
|
||||||
require_token=false # désactive le token validation (curl/Postman direct)
|
require_token=false # désactive le token validation (curl/Postman direct)
|
||||||
require_process_check=false # désactive le PID-on-socket check
|
require_process_check=false # désactive le PID-on-socket check
|
||||||
@@ -264,7 +265,17 @@ Le serveur Indy n'accepte une requête que si elle a :
|
|||||||
- OU cookie `pm_token=<token>; HttpOnly; SameSite=Strict; Path=/`
|
- OU cookie `pm_token=<token>; HttpOnly; SameSite=Strict; Path=/`
|
||||||
|
|
||||||
Au start :
|
Au start :
|
||||||
- Port aléatoire dans `[49152, 65535]` si pas configuré
|
- Port aléatoire dans `[49152, 65535]` si pas configuré. Tiré de
|
||||||
|
`PM.Crypto.RandomBytes` (BCryptGenRandom), **pas** de `Random` : `Randomize`
|
||||||
|
n'est appelé nulle part, donc `Random` rendait la même suite à chaque
|
||||||
|
lancement — le port « aléatoire » était en réalité le même tous les jours, ce
|
||||||
|
qui vidait de son sens l'aléatoire censé compliquer un balayage.
|
||||||
|
- **Port occupé → jusqu'à 3 essais** (`BindAttempts`). Le premier honore le port
|
||||||
|
demandé ; les suivants prennent un éphémère. Se lier EST le test : un sondage
|
||||||
|
préalable serait plus faible (un service qui écoute sans répondre passerait
|
||||||
|
inaperçu) et sujet à une course (le port peut être pris entre le sondage et le
|
||||||
|
lien). Chaque déplacement est journalisé — un serveur qui change de port en
|
||||||
|
silence est pire qu'un serveur qui refuse de démarrer.
|
||||||
- Token random 32-chars hex généré
|
- Token random 32-chars hex généré
|
||||||
- WebView2 navigate vers `http://127.0.0.1:{port}/index.html?pmt={token}`
|
- WebView2 navigate vers `http://127.0.0.1:{port}/index.html?pmt={token}`
|
||||||
- JS strip le `?pmt=...` du URL bar via `history.replaceState`
|
- JS strip le `?pmt=...` du URL bar via `history.replaceState`
|
||||||
|
|||||||
@@ -117,10 +117,31 @@ procedure TPMHTTPServer.Start(APort: Integer; SameFolder: Boolean;
|
|||||||
const
|
const
|
||||||
EphemeralPortMin = 49152;
|
EphemeralPortMin = 49152;
|
||||||
EphemeralPortMax = 65535;
|
EphemeralPortMax = 65535;
|
||||||
|
// Trois essais : le port demande, puis deux replis. Au-dela, ce n'est plus
|
||||||
|
// un port occupe mais une pile reseau qui ne veut pas de nous, et insister
|
||||||
|
// ne ferait que retarder le message d'erreur.
|
||||||
|
BindAttempts = 3;
|
||||||
|
|
||||||
|
/// Un port ephemere tire du generateur cryptographique de Windows.
|
||||||
|
///
|
||||||
|
/// PAS Random() : Randomize n'est appele nulle part dans ce programme, donc
|
||||||
|
/// Random rend la MEME suite a chaque lancement - le port "aleatoire" de
|
||||||
|
/// `port=0` etait en realite le meme tous les jours, ce qui vide de son sens
|
||||||
|
/// l'aleatoire cense compliquer un balayage exterieur. RandomBytes passe par
|
||||||
|
/// BCryptGenRandom, deja utilise pour le jeton d'acces.
|
||||||
|
function PickEphemeralPort: Integer;
|
||||||
|
var
|
||||||
|
LRaw: TBytes;
|
||||||
|
begin
|
||||||
|
LRaw := PM.Crypto.RandomBytes(2);
|
||||||
|
Result := EphemeralPortMin +
|
||||||
|
((LRaw[0] shl 8 or LRaw[1]) mod (EphemeralPortMax - EphemeralPortMin + 1));
|
||||||
|
end;
|
||||||
|
|
||||||
var
|
var
|
||||||
LBinding: TIdSocketHandle;
|
LBinding: TIdSocketHandle;
|
||||||
LDBPath, LWebRoot, LResolvedPort: string;
|
LDBPath, LWebRoot, LResolvedPort: string;
|
||||||
LRequestedPort: Integer;
|
LRequestedPort, LAttempt: Integer;
|
||||||
begin
|
begin
|
||||||
if FServer.Active then Exit;
|
if FServer.Active then Exit;
|
||||||
|
|
||||||
@@ -133,7 +154,7 @@ begin
|
|||||||
|
|
||||||
LRequestedPort := APort;
|
LRequestedPort := APort;
|
||||||
if LRequestedPort = 0 then
|
if LRequestedPort = 0 then
|
||||||
LRequestedPort := EphemeralPortMin + Random(EphemeralPortMax - EphemeralPortMin);
|
LRequestedPort := PickEphemeralPort;
|
||||||
|
|
||||||
var pathParent := '..\';
|
var pathParent := '..\';
|
||||||
if SameFolder then
|
if SameFolder then
|
||||||
@@ -146,12 +167,41 @@ begin
|
|||||||
Log('Web root: ' + LWebRoot);
|
Log('Web root: ' + LWebRoot);
|
||||||
InitStaticServer(LWebRoot);
|
InitStaticServer(LWebRoot);
|
||||||
|
|
||||||
FServer.Bindings.Clear;
|
// SE LIER EST LE TEST, ET IL N'Y EN A PAS D'AUTRE QUI VAILLE.
|
||||||
LBinding := FServer.Bindings.Add;
|
//
|
||||||
LBinding.IP := '127.0.0.1';
|
// On pourrait sonder le port avant, pour voir si quelqu'un repond. Ce serait
|
||||||
LBinding.Port := LRequestedPort;
|
// a la fois plus faible et plus fragile : un service qui ecoute sans repondre
|
||||||
|
// passerait inapercu, et le port peut etre pris entre le sondage et le lien.
|
||||||
FServer.Active := True;
|
// Tenter le lien tranche pour de bon, sans course.
|
||||||
|
//
|
||||||
|
// Le PREMIER essai honore le port demande - celui de config.txt, que
|
||||||
|
// l'utilisateur a peut-etre choisi expres. Les suivants prennent un port
|
||||||
|
// ephemere, exactement ce que fait un lancement sans port configure. Et on le
|
||||||
|
// DIT a chaque fois : un serveur qui deplace son port en silence est pire
|
||||||
|
// qu'un serveur qui refuse de demarrer, parce que rien n'explique ensuite
|
||||||
|
// pourquoi une adresse notee quelque part ne repond plus.
|
||||||
|
for LAttempt := 1 to BindAttempts do
|
||||||
|
begin
|
||||||
|
FServer.Bindings.Clear;
|
||||||
|
LBinding := FServer.Bindings.Add;
|
||||||
|
LBinding.IP := '127.0.0.1';
|
||||||
|
LBinding.Port := LRequestedPort;
|
||||||
|
try
|
||||||
|
FServer.Active := True;
|
||||||
|
Break;
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
FServer.Active := False; // Indy peut laisser le lien a moitie ouvert
|
||||||
|
Log(Format('Port %d unavailable: %s', [LRequestedPort, E.Message]));
|
||||||
|
// Le dernier echec remonte tel quel : l'appelant sait deja l'afficher.
|
||||||
|
if LAttempt = BindAttempts then raise;
|
||||||
|
LRequestedPort := PickEphemeralPort;
|
||||||
|
Log(Format(' falling back to port %d (attempt %d of %d)',
|
||||||
|
[LRequestedPort, LAttempt + 1, BindAttempts]));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
FBoundPort := LRequestedPort;
|
FBoundPort := LRequestedPort;
|
||||||
LResolvedPort := IntToStr(FBoundPort);
|
LResolvedPort := IntToStr(FBoundPort);
|
||||||
Log(Format('Server started on http://127.0.0.1:%s (token:%s process_check:%s)',
|
Log(Format('Server started on http://127.0.0.1:%s (token:%s process_check:%s)',
|
||||||
|
|||||||
Reference in New Issue
Block a user