diff --git a/CLAUDE.md b/CLAUDE.md index 03db378..574aae9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1443,6 +1443,15 @@ d'accord. ### Contraintes connues autofill +- **VNC (2026-10-14)** : les caracteres partent en `KEYEVENTF_UNICODE` + (`VK_PACKET`), que TigerVNC & co ne savent pas retransmettre - un Mac + recevait un `V` par caractere. Cible dont l'exe contient « vnc » + (`TargetTypesByKeyCodes`) → **vraies touches** (`SendKeyCodeString` : + `VkKeyScanEx` dans la disposition de la cible, + Maj/Ctrl/Alt, AltGr = + Ctrl+Alt) ; un caractere sans touche repart en Unicode. Journal : `typed as + key codes (VNC)` sur la ligne `Autofill waits`. Eprouve sur un mot de passe + simple ; caracteres speciaux dependants de la disposition du Mac distant. + - **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin, regedit → autofill silencieusement no-op. Tester avec apps non-élevées. - **Chrome password manager** : si Chrome a des credentials sauvegardés diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 660b72f..28e2d7a 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -2076,6 +2076,91 @@ begin SendInput(LCount, @LInputs[0], SizeOf(TInput)); end; +function QueryFullProcessImageNameW(hProcess: THandle; dwFlags: DWORD; + lpExeName: PWideChar; var lpdwSize: DWORD): BOOL; stdcall; + external kernel32 name 'QueryFullProcessImageNameW'; + +/// UN VISUALISEUR VNC NE SAIT PAS TAPER DE L'UNICODE. SendUnicodeString envoie +/// chaque caractere en VK_PACKET ; une fenetre Windows le comprend, mais VNC +/// (TigerVNC, RealVNC, TightVNC, UltraVNC) traduit les VRAIES touches vers la +/// machine distante et ne sait que faire de VK_PACKET : un Mac recevait +/// "VVVVVV", un V par caractere. Pour lui on tape les touches du clavier. +/// ponytail: reconnu au nom de l'exe ("vnc") ; un autre client a distance qui +/// aurait le meme defaut s'ajouterait ici, ou deviendrait un reglage. +function TargetTypesByKeyCodes(AHwnd: HWND): Boolean; +const + PROCESS_QUERY_LIMITED_INFORMATION = $1000; +var + LPid, LLen: DWORD; + LProc: THandle; + LName: array[0..MAX_PATH] of WideChar; +begin + Result := False; + LPid := 0; + GetWindowThreadProcessId(AHwnd, LPid); + if LPid = 0 then Exit; + LProc := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, LPid); + if LProc = 0 then Exit; + try + LLen := Length(LName); + if QueryFullProcessImageNameW(LProc, 0, @LName[0], LLen) then + Result := ExtractFileName(string(LName)).ToLower.Contains('vnc'); + finally + CloseHandle(LProc); + end; +end; + +/// Le texte en VRAIES touches, dans la disposition clavier de la cible : chaque +/// caractere devient sa touche plus Maj / Ctrl / Alt (AltGr = Ctrl+Alt) selon +/// VkKeyScanEx. Un caractere sans touche dans cette disposition part en +/// Unicode, faute de mieux. +procedure SendKeyCodeString(const AText: string; ALayout: HKL); +var + LInputs: TArray; + LCount: Integer; + procedure Key(AVk: Word; AUp: Boolean); + begin + if LCount >= Length(LInputs) then SetLength(LInputs, LCount + 16); + FillChar(LInputs[LCount], SizeOf(TInput), 0); + LInputs[LCount].Itype := INPUT_KEYBOARD; + LInputs[LCount].ki.wVk := AVk; + if AUp then LInputs[LCount].ki.dwFlags := KEYEVENTF_KEYUP; + Inc(LCount); + end; +var + I: Integer; + LScan: SHORT; + LVk, LMods: Byte; +begin + for I := 1 to Length(AText) do + begin + LScan := VkKeyScanEx(AText[I], ALayout); + if LScan = -1 then + begin + SendUnicodeString(AText[I]); + Continue; + end; + LVk := Lo(Word(LScan)); + LMods := Hi(Word(LScan)); + LCount := 0; + if LMods and 1 <> 0 then Key(VK_SHIFT, False); + if LMods and 2 <> 0 then Key(VK_CONTROL, False); + if LMods and 4 <> 0 then Key(VK_MENU, False); + Key(LVk, False); + Key(LVk, True); + if LMods and 4 <> 0 then Key(VK_MENU, True); + if LMods and 2 <> 0 then Key(VK_CONTROL, True); + if LMods and 1 <> 0 then Key(VK_SHIFT, True); + SendInput(LCount, @LInputs[0], SizeOf(TInput)); + end; +end; + +procedure SendTextTo(const AText: string; AByKeys: Boolean; ALayout: HKL); +begin + if AByKeys then SendKeyCodeString(AText, ALayout) + else SendUnicodeString(AText); +end; + // Send one virtual-key press (down+up). procedure SendVKey(AVk: Word); var @@ -2290,8 +2375,11 @@ begin LSw := TStopwatch.StartNew; WaitForModifierRelease(1000); ForceReleaseModifiers; // timeout hit with keys still down → clean state + var LByKeys := (ATargetHWND <> 0) and TargetTypesByKeyCodes(ATargetHWND); + var LLayout := GetKeyboardLayout(GetWindowThreadProcessId(ATargetHWND, nil)); FLastFillWaits := Format('foreground=%d ms, modifiers=%d ms', [LFgMs, LSw.ElapsedMilliseconds]); + if LByKeys then FLastFillWaits := FLastFillWaits + ', typed as key codes (VNC)'; Sleep(FocusSettleDelayMs); // Never type into our own window: if the target refused the foreground, @@ -2315,19 +2403,19 @@ begin if AUsernameOnly then begin ClearFieldIfWanted; - SendUnicodeString(AUsername); + SendTextTo(AUsername, LByKeys, LLayout); Exit; end; if AUsername = '' then begin ClearFieldIfWanted; - SendUnicodeString(APassword); + SendTextTo(APassword, LByKeys, LLayout); Exit; end; ClearFieldIfWanted; - SendUnicodeString(AUsername); + SendTextTo(AUsername, LByKeys, LLayout); Sleep(200); // Real-VK send: a physically held Shift here turns Tab into Shift+Tab — // focus moves BACKWARD and the password lands in the username field. @@ -2339,7 +2427,7 @@ begin // Tab lands somewhere that is not an editable field (a link, a reveal // button, the document itself, as on the Reddit login form) it selects the // ENTIRE PAGE, and the password is then typed into nothing. - SendUnicodeString(APassword); + SendTextTo(APassword, LByKeys, LLayout); finally // Re-register with the LIVE combo (user may have rebound it), not the // hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A.