mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: move opkssh and warpgate into plugins (C3)
This commit is contained in:
1 parent
925da4a661
commit
98d367d7e2
336 files changed
+22380
-4746
No files matched your search
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Open Link",
|
||||
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached",
|
||||
"noHostSelected": "No host selected",
|
||||
"opksshAuthFailed": "Authentication failed. Please check your credentials and try again.",
|
||||
"opksshTimeout": "Authentication timed out. Please try again.",
|
||||
"passwordPromptFillTitle": "Fill the saved password into this prompt?",
|
||||
"reconnect": "Reconnect",
|
||||
"reconnected": "Reconnected successfully",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Maak skakel oop",
|
||||
"maxReconnectAttemptsReached": "Maksimum herverbindingspogings bereik",
|
||||
"noHostSelected": "Geen gasheer gekies nie",
|
||||
"opksshAuthFailed": "Verifikasie het misluk. Kontroleer asseblief jou geloofsbriewe en probeer weer.",
|
||||
"opksshTimeout": "Verifikasie het verstryk. Probeer asseblief weer.",
|
||||
"passwordPromptFillTitle": "Vul die gestoorde wagwoord in hierdie aanwysingsprompt in?",
|
||||
"reconnect": "Herkoppel",
|
||||
"reconnected": "Herverbind suksesvol",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "فتح الرابط",
|
||||
"maxReconnectAttemptsReached": "تم الوصول إلى الحد الأقصى لمحاولات إعادة الاتصال",
|
||||
"noHostSelected": "لم يتم تحديد مضيف",
|
||||
"opksshAuthFailed": "فشلت المصادقة. يرجى التحقق من بيانات الاعتماد والمحاولة مرة أخرى.",
|
||||
"opksshTimeout": "انتهت مهلة المصادقة. يرجى المحاولة مرة أخرى.",
|
||||
"passwordPromptFillTitle": "هل تريد ملء كلمة المرور المحفوظة في هذه النافذة؟",
|
||||
"reconnect": "إعادة الاتصال",
|
||||
"reconnected": "تمت إعادة الاتصال بنجاح",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Отвори линка",
|
||||
"maxReconnectAttemptsReached": "Достигнат е максималният брой опити за повторно свързване",
|
||||
"noHostSelected": "Няма избран хост",
|
||||
"opksshAuthFailed": "Удостоверяването не бе успешно. Моля, проверете идентификационните си данни и опитайте отново.",
|
||||
"opksshTimeout": "Времето за изчакване на удостоверяването изтече. Моля, опитайте отново.",
|
||||
"passwordPromptFillTitle": "Да се попълни ли запазената парола в този ред?",
|
||||
"reconnect": "Свържете се отново",
|
||||
"reconnected": "Успешно повторно свързване",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "লিঙ্ক খুলুন",
|
||||
"maxReconnectAttemptsReached": "পুনঃসংযোগের সর্বোচ্চ প্রচেষ্টা শেষ হয়েছে",
|
||||
"noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি",
|
||||
"opksshAuthFailed": "প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আপনার পরিচয়পত্র যাচাই করে আবার চেষ্টা করুন।",
|
||||
"opksshTimeout": "প্রমাণীকরণের সময়সীমা শেষ হয়ে গেছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
|
||||
"passwordPromptFillTitle": "সংরক্ষিত পাসওয়ার্ডটি এই প্রম্পটে পূরণ করুন?",
|
||||
"reconnect": "পুনরায় সংযোগ করুন",
|
||||
"reconnected": "সফলভাবে পুনরায় সংযুক্ত হয়েছে",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Obre l'enllaç",
|
||||
"maxReconnectAttemptsReached": "S'ha arribat al màxim d'intents de reconnexió",
|
||||
"noHostSelected": "No s'ha seleccionat cap amfitrió",
|
||||
"opksshAuthFailed": "L'autenticació ha fallat. Si us plau, comproveu les vostres credencials i torneu-ho a intentar.",
|
||||
"opksshTimeout": "S'ha esgotat el temps d'autenticació. Torna-ho a provar.",
|
||||
"passwordPromptFillTitle": "Voleu introduir la contrasenya desada en aquesta sol·licitud?",
|
||||
"reconnect": "Reconnecta",
|
||||
"reconnected": "S'ha reconnectat correctament",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Otevřít odkaz",
|
||||
"maxReconnectAttemptsReached": "Dosažen maximální počet pokusů o opětovné připojení",
|
||||
"noHostSelected": "Nebyl vybrán žádný hostitel",
|
||||
"opksshAuthFailed": "Ověření se nezdařilo. Zkontrolujte prosím své přihlašovací údaje a zkuste to znovu.",
|
||||
"opksshTimeout": "Časový limit ověření vypršel. Zkuste to prosím znovu.",
|
||||
"passwordPromptFillTitle": "Vyplňte uložené heslo do tohoto řádku?",
|
||||
"reconnect": "Znovu se připojte",
|
||||
"reconnected": "Úspěšné opětovné připojení",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Åbn link",
|
||||
"maxReconnectAttemptsReached": "Maksimalt antal forsøg på genoprettelse nået",
|
||||
"noHostSelected": "Ingen vært valgt",
|
||||
"opksshAuthFailed": "Godkendelse mislykkedes. Tjek dine loginoplysninger, og prøv igen.",
|
||||
"opksshTimeout": "Godkendelsen udløb. Prøv igen.",
|
||||
"passwordPromptFillTitle": "Udfyld den gemte adgangskode i denne prompt?",
|
||||
"reconnect": "Genopret forbindelse",
|
||||
"reconnected": "Genoprettet forbindelse",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Link öffnen",
|
||||
"maxReconnectAttemptsReached": "Maximale Wiederverbindungsversuche erreicht",
|
||||
"noHostSelected": "Kein Host ausgewählt",
|
||||
"opksshAuthFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldeinformationen und versuchen Sie es erneut.",
|
||||
"opksshTimeout": "Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.",
|
||||
"passwordPromptFillTitle": "Gespeichertes Passwort in diese Eingabeaufforderung einfügen?",
|
||||
"reconnect": "Erneut verbinden",
|
||||
"reconnected": "Erfolgreich wieder verbunden",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Άνοιγμα συνδέσμου",
|
||||
"maxReconnectAttemptsReached": "Επιτεύχθηκε ο μέγιστος αριθμός προσπαθειών επανασύνδεσης",
|
||||
"noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής",
|
||||
"opksshAuthFailed": "Ο έλεγχος ταυτότητας απέτυχε. Ελέγξτε τα διαπιστευτήριά σας και προσπαθήστε ξανά.",
|
||||
"opksshTimeout": "Το χρονικό όριο ελέγχου ταυτότητας έληξε. Δοκιμάστε ξανά.",
|
||||
"passwordPromptFillTitle": "Συμπληρώστε τον αποθηκευμένο κωδικό πρόσβασης σε αυτήν την ερώτηση;",
|
||||
"reconnect": "Επανασύνδεση",
|
||||
"reconnected": "Επανασύνδεση με επιτυχία",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Abrir enlace",
|
||||
"maxReconnectAttemptsReached": "Número máximo de intentos de reconexión alcanzado",
|
||||
"noHostSelected": "Ningún host seleccionado",
|
||||
"opksshAuthFailed": "La autenticación falló. Revisa tus credenciales e inténtalo de nuevo.",
|
||||
"opksshTimeout": "La autenticación expiró. Inténtalo de nuevo.",
|
||||
"passwordPromptFillTitle": "¿Rellenar este campo con la contraseña guardada?",
|
||||
"reconnect": "Reconectar",
|
||||
"reconnected": "Reconectado correctamente",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Avaa linkki",
|
||||
"maxReconnectAttemptsReached": "Yhteyden muodostamisen uudelleenyritysten enimmäismäärä saavutettu",
|
||||
"noHostSelected": "Ei valittua isäntää",
|
||||
"opksshAuthFailed": "Todennus epäonnistui. Tarkista tunnistetietosi ja yritä uudelleen.",
|
||||
"opksshTimeout": "Todennus aikakatkaistiin. Yritä uudelleen.",
|
||||
"passwordPromptFillTitle": "Täytä tallennettu salasana tähän kehotteeseen?",
|
||||
"reconnect": "Yhdistä uudelleen",
|
||||
"reconnected": "Yhdistäminen uudelleen onnistui",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Ouvrir le lien",
|
||||
"maxReconnectAttemptsReached": "Nombre maximal de tentatives de reconnexion atteint",
|
||||
"noHostSelected": "Aucun hôte sélectionné",
|
||||
"opksshAuthFailed": "Échec de l'authentification. Veuillez vérifier vos identifiants et réessayer.",
|
||||
"opksshTimeout": "L'authentification a expiré. Veuillez réessayer.",
|
||||
"passwordPromptFillTitle": "Remplir ce champ avec le mot de passe enregistré ?",
|
||||
"reconnect": "Reconnecter",
|
||||
"reconnected": "Reconnexion réussie",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "פתח קישור",
|
||||
"maxReconnectAttemptsReached": "הגעת למספר המקסימלי של ניסיונות חיבור מחדש",
|
||||
"noHostSelected": "לא נבחר מארח",
|
||||
"opksshAuthFailed": "האימות נכשל. אנא בדוק את פרטי הגישה שלך ונסה שוב.",
|
||||
"opksshTimeout": "הזמן שהוקצב לאימות הסתיים. אנא נסה שוב.",
|
||||
"passwordPromptFillTitle": "למלא את הסיסמה השמורה בשורת הפקודה הזו?",
|
||||
"reconnect": "התחבר מחדש",
|
||||
"reconnected": "התחבר מחדש בהצלחה",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "लिंक खोलें",
|
||||
"maxReconnectAttemptsReached": "अधिकतम पुनः कनेक्ट प्रयास पूर्ण हो गए",
|
||||
"noHostSelected": "कोई होस्ट चयनित नहीं",
|
||||
"opksshAuthFailed": "प्रमाणीकरण विफल। कृपया अपने क्रेडेंशियल जाँचें और पुनः प्रयास करें।",
|
||||
"opksshTimeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।",
|
||||
"passwordPromptFillTitle": "क्या सहेजे गए पासवर्ड को इस प्रांप्ट में भरें?",
|
||||
"reconnect": "पुनः कनेक्ट करें",
|
||||
"reconnected": "पुनः कनेक्ट हुआ",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Link megnyitása",
|
||||
"maxReconnectAttemptsReached": "Elérte az újracsatlakozási kísérletek maximális számát",
|
||||
"noHostSelected": "Nincs kiválasztva gazdagép",
|
||||
"opksshAuthFailed": "Sikertelen hitelesítés. Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra.",
|
||||
"opksshTimeout": "A hitelesítés időtúllépést okozott. Kérjük, próbálja újra.",
|
||||
"passwordPromptFillTitle": "Írja be a mentett jelszót ebbe a mezőbe?",
|
||||
"reconnect": "Újracsatlakozás",
|
||||
"reconnected": "Sikeresen újrakapcsolódott",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Buka Tautan",
|
||||
"maxReconnectAttemptsReached": "Upaya penyambungan kembali maksimum telah tercapai.",
|
||||
"noHostSelected": "Tidak ada pembawa acara yang dipilih.",
|
||||
"opksshAuthFailed": "Autentikasi gagal. Harap periksa kredensial Anda dan coba lagi.",
|
||||
"opksshTimeout": "Autentikasi habis waktu. Silakan coba lagi.",
|
||||
"passwordPromptFillTitle": "Masukkan kata sandi yang tersimpan ke dalam kolom ini?",
|
||||
"reconnect": "Terhubung kembali",
|
||||
"reconnected": "Berhasil terhubung kembali",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Apri collegamento",
|
||||
"maxReconnectAttemptsReached": "Raggiunto il numero massimo di tentativi di riconnessione",
|
||||
"noHostSelected": "Nessun host selezionato",
|
||||
"opksshAuthFailed": "Autenticazione fallita. Controlla le credenziali e riprova.",
|
||||
"opksshTimeout": "Autenticazione scaduta. Riprova.",
|
||||
"passwordPromptFillTitle": "Inserire la password salvata in questa richiesta?",
|
||||
"reconnect": "Riconnetti",
|
||||
"reconnected": "Riconnesso con successo",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "リンクを開く",
|
||||
"maxReconnectAttemptsReached": "再接続の最大試行回数に達しました",
|
||||
"noHostSelected": "ホストが選択されていません",
|
||||
"opksshAuthFailed": "認証に失敗しました。資格情報を確認して再試行してください。",
|
||||
"opksshTimeout": "認証がタイムアウトしました。もう一度お試しください。",
|
||||
"passwordPromptFillTitle": "保存されたパスワードをこのプロンプトに入力しますか?",
|
||||
"reconnect": "再接続",
|
||||
"reconnected": "再接続完了",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "링크 열기",
|
||||
"maxReconnectAttemptsReached": "최대 재연결 시도 횟수에 도달했습니다.",
|
||||
"noHostSelected": "선택된 호스트가 없습니다.",
|
||||
"opksshAuthFailed": "인증에 실패했습니다. 자격 증명을 확인하고 다시 시도하세요.",
|
||||
"opksshTimeout": "인증 시간이 초과되었습니다. 다시 시도하세요.",
|
||||
"passwordPromptFillTitle": "저장된 비밀번호를 이 프롬프트에 입력하시겠습니까?",
|
||||
"reconnect": "재연결",
|
||||
"reconnected": "재연결 성공",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Open link",
|
||||
"maxReconnectAttemptsReached": "Maximaal aantal herverbindingspogingen bereikt",
|
||||
"noHostSelected": "Geen host geselecteerd",
|
||||
"opksshAuthFailed": "Authenticatie mislukt. Controleer uw inloggegevens en probeer het opnieuw.",
|
||||
"opksshTimeout": "De authenticatie is mislukt vanwege een time-out. Probeer het opnieuw.",
|
||||
"passwordPromptFillTitle": "Voer het opgeslagen wachtwoord in op dit veld.",
|
||||
"reconnect": "Opnieuw verbinding maken",
|
||||
"reconnected": "De verbinding is succesvol hersteld.",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Åpne lenke",
|
||||
"maxReconnectAttemptsReached": "Maksimalt antall forsøk på å koble til igjen er nådd",
|
||||
"noHostSelected": "Ingen vert valgt",
|
||||
"opksshAuthFailed": "Autentiseringen mislyktes. Sjekk legitimasjonen din og prøv på nytt.",
|
||||
"opksshTimeout": "Autentiseringen ble tidsavbrutt. Prøv på nytt.",
|
||||
"passwordPromptFillTitle": "Skriv inn det lagrede passordet i denne meldingen?",
|
||||
"reconnect": "Koble til igjen",
|
||||
"reconnected": "Tilkoblet på nytt",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Otwórz link",
|
||||
"maxReconnectAttemptsReached": "Osiągnięto maksymalną liczbę prób ponownego połączenia",
|
||||
"noHostSelected": "Nie wybrano hosta",
|
||||
"opksshAuthFailed": "Uwierzytelnienie nie powiodło się. Sprawdź poświadczenia i spróbuj ponownie.",
|
||||
"opksshTimeout": "Upłynął limit czasu uwierzytelniania. Spróbuj ponownie.",
|
||||
"passwordPromptFillTitle": "Czy wpisać zapisane hasło w tym monicie?",
|
||||
"reconnect": "Połącz ponownie",
|
||||
"reconnected": "Połączono ponownie pomyślnie",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Abrir link",
|
||||
"maxReconnectAttemptsReached": "Número máximo de tentativas de reconexão atingido",
|
||||
"noHostSelected": "Nenhum host selecionado",
|
||||
"opksshAuthFailed": "A autenticação falhou. Verifique suas credenciais e tente novamente.",
|
||||
"opksshTimeout": "A autenticação expirou. Tente novamente.",
|
||||
"passwordPromptFillTitle": "Digite a senha salva neste campo?",
|
||||
"reconnect": "Reconectar",
|
||||
"reconnected": "Reconectado com sucesso",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Abrir Ligação",
|
||||
"maxReconnectAttemptsReached": "Número máximo de tentativas de restabelecimento de ligação atingido",
|
||||
"noHostSelected": "Nenhum host selecionado",
|
||||
"opksshAuthFailed": "A autenticação falhou. Verifique as suas credenciais e tente novamente.",
|
||||
"opksshTimeout": "A autenticação expirou. Tente novamente.",
|
||||
"passwordPromptFillTitle": "Preencher a palavra-passe guardada neste pedido?",
|
||||
"reconnect": "Reconectar",
|
||||
"reconnected": "Ligação restabelecida com sucesso",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Deschideți linkul",
|
||||
"maxReconnectAttemptsReached": "S-a atins numărul maxim de încercări de reconectare",
|
||||
"noHostSelected": "Nicio gazdă selectată",
|
||||
"opksshAuthFailed": "Autentificarea a eșuat. Verificați acreditările și încercați din nou.",
|
||||
"opksshTimeout": "Autentificarea a expirat. Vă rugăm să încercați din nou.",
|
||||
"passwordPromptFillTitle": "Introduceți parola salvată în această solicitare?",
|
||||
"reconnect": "Reconectați-vă",
|
||||
"reconnected": "Reconectare cu succes",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Открыть ссылку",
|
||||
"maxReconnectAttemptsReached": "Достигнуто максимальное количество попыток переподключения",
|
||||
"noHostSelected": "Не выбран хост",
|
||||
"opksshAuthFailed": "Аутентификация не удалась. Проверьте учётные данные и попробуйте снова.",
|
||||
"opksshTimeout": "Время аутентификации истекло. Попробуйте снова.",
|
||||
"passwordPromptFillTitle": "Вставить сохраненный пароль в этот запрос?",
|
||||
"reconnect": "Переподключиться",
|
||||
"reconnected": "Успешно переподключено",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Отвори линк",
|
||||
"maxReconnectAttemptsReached": "Достигнут је максималан број покушаја поновног повезивања",
|
||||
"noHostSelected": "Није изабран домаћин",
|
||||
"opksshAuthFailed": "Аутентификација није успела. Проверите своје акредитиве и покушајте поново.",
|
||||
"opksshTimeout": "Временско ограничење за аутентификацију је истекло. Молимо покушајте поново.",
|
||||
"passwordPromptFillTitle": "Унесите сачувану лозинку у овај промпт?",
|
||||
"reconnect": "Поново се повежите",
|
||||
"reconnected": "Поново повезано успешно",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Öppna länk",
|
||||
"maxReconnectAttemptsReached": "Maximalt antal återanslutningsförsök uppnådda",
|
||||
"noHostSelected": "Ingen värd vald",
|
||||
"opksshAuthFailed": "Autentiseringen misslyckades. Kontrollera dina inloggningsuppgifter och försök igen.",
|
||||
"opksshTimeout": "Autentiseringen har gått ut. Försök igen.",
|
||||
"passwordPromptFillTitle": "Fyll i det sparade lösenordet i den här frågan?",
|
||||
"reconnect": "Återanslut",
|
||||
"reconnected": "Återansluten lyckades",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "เปิดลิงก์",
|
||||
"maxReconnectAttemptsReached": "จำนวนครั้งการเชื่อมต่อใหม่สูงสุดครบแล้ว",
|
||||
"noHostSelected": "ไม่ได้เลือกโฮสต์",
|
||||
"opksshAuthFailed": "การตรวจสอบสิทธิ์ล้มเหลว โปรดตรวจสอบข้อมูลประจำตัวของคุณและลองอีกครั้ง",
|
||||
"opksshTimeout": "การตรวจสอบสิทธิ์หมดเวลา โปรดลองอีกครั้ง",
|
||||
"passwordPromptFillTitle": "กรอกรหัสผ่านที่บันทึกไว้ลงในช่องนี้ใช่หรือไม่?",
|
||||
"reconnect": "เชื่อมต่อใหม่",
|
||||
"reconnected": "เชื่อมต่อสำเร็จแล้ว",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Bağlantıyı Aç",
|
||||
"maxReconnectAttemptsReached": "Maksimum yeniden bağlanma denemesine ulaşıldı",
|
||||
"noHostSelected": "Hiçbir sunucu seçilmedi",
|
||||
"opksshAuthFailed": "Kimlik doğrulaması başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edip tekrar deneyin.",
|
||||
"opksshTimeout": "Kimlik doğrulaması zaman aşımına uğradı. Lütfen tekrar deneyin.",
|
||||
"passwordPromptFillTitle": "Kayıtlı parola bu isteme doldurulsun mu?",
|
||||
"reconnect": "Yeniden Bağlan",
|
||||
"reconnected": "Başarıyla yeniden bağlandı",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Відкрити посилання",
|
||||
"maxReconnectAttemptsReached": "Досягнуто максимальної кількості спроб повторного підключення",
|
||||
"noHostSelected": "Хост не вибрано",
|
||||
"opksshAuthFailed": "Помилка автентифікації. Перевірте свої облікові дані та спробуйте ще раз.",
|
||||
"opksshTimeout": "Час очікування автентифікації минув. Спробуйте ще раз.",
|
||||
"passwordPromptFillTitle": "Введіть збережений пароль у цей запит?",
|
||||
"reconnect": "Знову підключитися",
|
||||
"reconnected": "Успішно відновлено підключення",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "Mở liên kết",
|
||||
"maxReconnectAttemptsReached": "Đã đạt số lần kết nối lại tối đa",
|
||||
"noHostSelected": "Chưa chọn máy chủ",
|
||||
"opksshAuthFailed": "Xác thực thất bại. Vui lòng kiểm tra thông tin đăng nhập và thử lại.",
|
||||
"opksshTimeout": "Xác thực đã hết thời gian chờ. Vui lòng thử lại.",
|
||||
"passwordPromptFillTitle": "Điền mật khẩu đã lưu vào dòng nhắc này?",
|
||||
"reconnect": "Kết nối lại",
|
||||
"reconnected": "Đã kết nối lại thành công",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "打开链接",
|
||||
"maxReconnectAttemptsReached": "已达到最大重连尝试次数",
|
||||
"noHostSelected": "未选择主机",
|
||||
"opksshAuthFailed": "认证失败,请检查凭据并重试。",
|
||||
"opksshTimeout": "认证超时,请重试。",
|
||||
"passwordPromptFillTitle": "将保存的密码填入此提示?",
|
||||
"reconnect": "重新连接",
|
||||
"reconnected": "重新连接成功",
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
"linkDialogTitle": "開啟連結",
|
||||
"maxReconnectAttemptsReached": "已達最大重新連線嘗試次數",
|
||||
"noHostSelected": "未選取主機",
|
||||
"opksshAuthFailed": "驗證失敗。請檢查您的憑證並再試一次。",
|
||||
"opksshTimeout": "驗證逾時。請再試一次。",
|
||||
"passwordPromptFillTitle": "將已儲存的密碼填入此提示?",
|
||||
"reconnect": "重新連線",
|
||||
"reconnected": "已成功重新連線",
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
*
|
||||
* Core's classifier (ctx.ssh.classifyKeyboardInteractive) decides what a
|
||||
* round is; this keeps the terminal's message protocol (totp_required,
|
||||
* totp_retry, password_required, warpgate_auth_required) and its timeouts
|
||||
* exactly as the client expects.
|
||||
* totp_retry, password_required, "<handler>_auth_required" for a browser
|
||||
* sign-in round) and its timeouts exactly as the client expects.
|
||||
*/
|
||||
|
||||
import type { WebSocket } from "ws";
|
||||
import type {
|
||||
PluginBrowserSignInRound,
|
||||
PluginKeyboardInteractivePrompt,
|
||||
PluginSsh,
|
||||
PluginSshHost,
|
||||
@@ -40,7 +41,6 @@ interface ResolvedCredentials {
|
||||
}
|
||||
|
||||
interface HostFlags {
|
||||
useWarpgate?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -54,15 +54,16 @@ interface AuthContext {
|
||||
keyboardInteractiveResponded: boolean;
|
||||
keyboardInteractiveFinish: ((responses: string[]) => void) | null;
|
||||
totpPromptSent: boolean;
|
||||
warpgateAuthPromptSent: boolean;
|
||||
/** The handler whose browser round is waiting for "<id>_auth_continue". */
|
||||
browserSignInId: string | null;
|
||||
totpTimeout: NodeJS.Timeout | null;
|
||||
warpgateAuthTimeout: NodeJS.Timeout | null;
|
||||
browserSignInTimeout: NodeJS.Timeout | null;
|
||||
totpAttempts: number;
|
||||
}
|
||||
|
||||
const TOTP_TIMEOUT_MS = 180000;
|
||||
const PUSH_TIMEOUT_MS = 300000;
|
||||
const WARPGATE_TIMEOUT_MS = 300000;
|
||||
const BROWSER_SIGN_IN_TIMEOUT_MS = 300000;
|
||||
|
||||
export class SSHAuthManager {
|
||||
public context: AuthContext;
|
||||
@@ -101,8 +102,8 @@ export class SSHAuthManager {
|
||||
case "auto":
|
||||
finish(decision.responses);
|
||||
return;
|
||||
case "warpgate":
|
||||
this.handleWarpgate(decision, finish);
|
||||
case "browser":
|
||||
this.handleBrowserSignIn(decision, finish);
|
||||
return;
|
||||
case "totp":
|
||||
this.handleTotp(
|
||||
@@ -124,40 +125,48 @@ export class SSHAuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
private handleWarpgate(
|
||||
decision: { url: string | null; securityKey: string; instructions: string },
|
||||
/**
|
||||
* A round finished in a browser. The message is named after the handler
|
||||
* that claimed it (Termix-Mobile listens for these names), and the client
|
||||
* answers with "<id>_auth_continue".
|
||||
*/
|
||||
private handleBrowserSignIn(
|
||||
round: PluginBrowserSignInRound,
|
||||
finish: (responses: string[]) => void,
|
||||
): void {
|
||||
this.context.keyboardInteractiveFinish = () => {
|
||||
finish([""]);
|
||||
};
|
||||
this.context.warpgateAuthPromptSent = true;
|
||||
this.sendLog("auth", "info", "Warpgate authentication required");
|
||||
this.context.browserSignInId = round.id;
|
||||
this.sendLog("auth", "info", `${round.label} sign-in required`);
|
||||
this.context.ws.send(
|
||||
JSON.stringify({
|
||||
type: "warpgate_auth_required",
|
||||
url: decision.url,
|
||||
securityKey: decision.securityKey,
|
||||
instructions: decision.instructions,
|
||||
type: `${round.id}_auth_required`,
|
||||
kind: "browser",
|
||||
label: round.label,
|
||||
url: round.url,
|
||||
securityKey: round.code,
|
||||
instructions: round.instructions,
|
||||
}),
|
||||
);
|
||||
|
||||
this.context.warpgateAuthTimeout = setTimeout(() => {
|
||||
this.context.browserSignInTimeout = setTimeout(() => {
|
||||
if (this.context.keyboardInteractiveFinish) {
|
||||
this.context.keyboardInteractiveFinish = null;
|
||||
this.context.warpgateAuthPromptSent = false;
|
||||
this.context.log.warn("Warpgate authentication timeout", {
|
||||
operation: "warpgate_timeout",
|
||||
this.context.browserSignInId = null;
|
||||
this.context.log.warn("Browser sign-in timeout", {
|
||||
operation: "browser_sign_in_timeout",
|
||||
handler: round.id,
|
||||
hostId: this.context.hostId,
|
||||
});
|
||||
this.context.ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: "Warpgate authentication timeout. Please reconnect.",
|
||||
message: `${round.label} sign-in timed out. Please reconnect.`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, WARPGATE_TIMEOUT_MS);
|
||||
}, BROWSER_SIGN_IN_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private handleTotp(
|
||||
@@ -289,13 +298,13 @@ export class SSHAuthManager {
|
||||
clearTimeout(this.context.totpTimeout);
|
||||
this.context.totpTimeout = null;
|
||||
}
|
||||
if (this.context.warpgateAuthTimeout) {
|
||||
clearTimeout(this.context.warpgateAuthTimeout);
|
||||
this.context.warpgateAuthTimeout = null;
|
||||
if (this.context.browserSignInTimeout) {
|
||||
clearTimeout(this.context.browserSignInTimeout);
|
||||
this.context.browserSignInTimeout = null;
|
||||
}
|
||||
this.context.keyboardInteractiveFinish = null;
|
||||
this.context.totpPromptSent = false;
|
||||
this.context.warpgateAuthPromptSent = false;
|
||||
this.context.browserSignInId = null;
|
||||
this.context.keyboardInteractiveResponded = false;
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ interface ConnectToHostData {
|
||||
credentialId?: number;
|
||||
userId?: string;
|
||||
forceKeyboardInteractive?: boolean;
|
||||
useWarpgate?: boolean;
|
||||
jumpHosts?: Array<{ hostId: number }>;
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
@@ -341,8 +340,9 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
let isCleaningUp = false;
|
||||
let isShellInitializing = false;
|
||||
let isDuplicateConnDiscarded = false;
|
||||
let warpgateAuthPromptSent = false;
|
||||
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
|
||||
// The keyboard-interactive handler whose browser sign-in is pending.
|
||||
let browserSignInId: string | null = null;
|
||||
let browserSignInTimeout: NodeJS.Timeout | null = null;
|
||||
let isAwaitingAuthCredentials = false;
|
||||
|
||||
let wsAlive = true;
|
||||
@@ -418,7 +418,7 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
keyboardInteractiveResponded = false;
|
||||
keyboardInteractiveFinish = null;
|
||||
totpPromptSent = false;
|
||||
warpgateAuthPromptSent = false;
|
||||
browserSignInId = null;
|
||||
}
|
||||
|
||||
ws.on(
|
||||
@@ -907,19 +907,6 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "warpgate_auth_continue": {
|
||||
if (keyboardInteractiveFinish) {
|
||||
if (warpgateAuthTimeout) {
|
||||
clearTimeout(warpgateAuthTimeout);
|
||||
warpgateAuthTimeout = null;
|
||||
}
|
||||
keyboardInteractiveFinish([""]);
|
||||
keyboardInteractiveFinish = null;
|
||||
warpgateAuthPromptSent = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "reconnect_with_credentials": {
|
||||
const credentialsData = data as {
|
||||
cols: number;
|
||||
@@ -1004,178 +991,6 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "opkssh_start_auth": {
|
||||
const opksshData = data as { hostId: number };
|
||||
try {
|
||||
await startAuthInteraction("opkssh", opksshData.hostId, data);
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to start OPKSSH auth", error, {
|
||||
operation: "opkssh_start_auth_error",
|
||||
userId,
|
||||
hostId: opksshData.hostId,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "opkssh_error",
|
||||
requestId: "",
|
||||
error:
|
||||
error instanceof PluginSshInteractionError
|
||||
? error.message
|
||||
: "Failed to start OPKSSH authentication",
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "opkssh_cancel": {
|
||||
const cancelData = data as { requestId: string };
|
||||
try {
|
||||
await ctx.ssh.cancelInteraction("opkssh", {
|
||||
requestId: String(cancelData?.requestId ?? ""),
|
||||
});
|
||||
resetConnectionState();
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to cancel OPKSSH auth", error, {
|
||||
operation: "opkssh_cancel_error",
|
||||
userId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "opkssh_browser_opened": {
|
||||
break;
|
||||
}
|
||||
|
||||
case "opkssh_auth_completed": {
|
||||
const completedData = data as {
|
||||
hostId: number;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
hostConfig?: ConnectToHostData["hostConfig"];
|
||||
};
|
||||
|
||||
resetConnectionState();
|
||||
|
||||
const reconnectConfig: ConnectToHostData = {
|
||||
cols: completedData.cols || 80,
|
||||
rows: completedData.rows || 24,
|
||||
hostConfig:
|
||||
completedData.hostConfig ||
|
||||
({
|
||||
id: completedData.hostId,
|
||||
ip: "",
|
||||
port: 22,
|
||||
username: "",
|
||||
userId,
|
||||
} as ConnectToHostData["hostConfig"]),
|
||||
};
|
||||
|
||||
handleConnectToHost(reconnectConfig).catch((error) => {
|
||||
sshLogger.error(
|
||||
"Failed to reconnect after OPKSSH auth",
|
||||
error,
|
||||
{
|
||||
operation: "opkssh_reconnect_error",
|
||||
userId,
|
||||
hostId: completedData.hostId,
|
||||
},
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message:
|
||||
"Failed to connect after authentication: " +
|
||||
getErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "vault_start_auth": {
|
||||
const vaultData = data as { hostId: number };
|
||||
try {
|
||||
await startAuthInteraction("vault", vaultData.hostId, data);
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to start Vault auth", error, {
|
||||
operation: "vault_start_auth_error",
|
||||
userId,
|
||||
hostId: vaultData.hostId,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "vault_error",
|
||||
hostId: vaultData.hostId,
|
||||
error: getErrorMessage(
|
||||
error,
|
||||
"Failed to start Vault authentication",
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "vault_cancel": {
|
||||
const cancelData = data as { hostId: number };
|
||||
try {
|
||||
await ctx.ssh.cancelInteraction("vault", {
|
||||
hostId: cancelData.hostId,
|
||||
});
|
||||
resetConnectionState();
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to cancel Vault auth", error, {
|
||||
operation: "vault_cancel_error",
|
||||
userId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "vault_auth_completed": {
|
||||
const completedData = data as {
|
||||
hostId: number;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
hostConfig?: ConnectToHostData["hostConfig"];
|
||||
};
|
||||
|
||||
resetConnectionState();
|
||||
|
||||
const reconnectConfig: ConnectToHostData = {
|
||||
cols: completedData.cols || 80,
|
||||
rows: completedData.rows || 24,
|
||||
hostConfig:
|
||||
completedData.hostConfig ||
|
||||
({
|
||||
id: completedData.hostId,
|
||||
ip: "",
|
||||
port: 22,
|
||||
username: "",
|
||||
userId,
|
||||
} as ConnectToHostData["hostConfig"]),
|
||||
};
|
||||
|
||||
handleConnectToHost(reconnectConfig).catch((error) => {
|
||||
sshLogger.error("Failed to reconnect after Vault auth", error, {
|
||||
operation: "vault_reconnect_error",
|
||||
userId,
|
||||
hostId: completedData.hostId,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message:
|
||||
"Failed to connect after authentication: " +
|
||||
getErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "collab_subscribe": {
|
||||
const { roomId } = (data ?? {}) as { roomId?: string };
|
||||
if (typeof roomId !== "string" || !roomId) break;
|
||||
@@ -1296,6 +1111,7 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
}
|
||||
|
||||
default:
|
||||
if (await handleSignInMessage(type, data)) break;
|
||||
sshLogger.warn("Unknown message type received", {
|
||||
operation: "websocket_message_unknown_type",
|
||||
userId,
|
||||
@@ -1340,6 +1156,122 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages a sign-in's UI sends: "<interaction>_start_auth", "_cancel"
|
||||
* and "_auth_completed" for a provider's browser step, and
|
||||
* "<handler>_auth_continue" for a pending keyboard-interactive browser
|
||||
* round. False for anything else.
|
||||
*/
|
||||
async function handleSignInMessage(
|
||||
type: string,
|
||||
data: unknown,
|
||||
): Promise<boolean> {
|
||||
const match =
|
||||
/^([a-z0-9-]+)_(start_auth|cancel|auth_completed|auth_continue)$/.exec(
|
||||
type,
|
||||
);
|
||||
if (!match) return false;
|
||||
const [, name, action] = match;
|
||||
const payload = (data ?? {}) as Record<string, unknown>;
|
||||
const hostId = Number(payload.hostId);
|
||||
|
||||
if (action === "start_auth") {
|
||||
try {
|
||||
await startAuthInteraction(name, hostId, payload);
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to start sign-in", error, {
|
||||
operation: "sign_in_start_error",
|
||||
interaction: name,
|
||||
userId,
|
||||
hostId,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: `${name}_error`,
|
||||
requestId: "",
|
||||
hostId,
|
||||
error:
|
||||
error instanceof PluginSshInteractionError
|
||||
? error.message
|
||||
: getErrorMessage(error, "Failed to start authentication"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action === "cancel") {
|
||||
try {
|
||||
await ctx.ssh.cancelInteraction(name, {
|
||||
requestId:
|
||||
typeof payload.requestId === "string"
|
||||
? payload.requestId
|
||||
: undefined,
|
||||
hostId: Number.isInteger(hostId) ? hostId : undefined,
|
||||
});
|
||||
resetConnectionState();
|
||||
} catch (error) {
|
||||
sshLogger.error("Failed to cancel sign-in", error, {
|
||||
operation: "sign_in_cancel_error",
|
||||
interaction: name,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action === "auth_continue") {
|
||||
if (name === browserSignInId && keyboardInteractiveFinish) {
|
||||
if (browserSignInTimeout) {
|
||||
clearTimeout(browserSignInTimeout);
|
||||
browserSignInTimeout = null;
|
||||
}
|
||||
keyboardInteractiveFinish([""]);
|
||||
keyboardInteractiveFinish = null;
|
||||
browserSignInId = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const completed = payload as {
|
||||
hostId: number;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
hostConfig?: ConnectToHostData["hostConfig"];
|
||||
};
|
||||
resetConnectionState();
|
||||
const reconnectConfig: ConnectToHostData = {
|
||||
cols: completed.cols || 80,
|
||||
rows: completed.rows || 24,
|
||||
hostConfig:
|
||||
completed.hostConfig ||
|
||||
({
|
||||
id: completed.hostId,
|
||||
ip: "",
|
||||
port: 22,
|
||||
username: "",
|
||||
userId,
|
||||
} as ConnectToHostData["hostConfig"]),
|
||||
};
|
||||
handleConnectToHost(reconnectConfig).catch((error) => {
|
||||
sshLogger.error("Failed to reconnect after sign-in", error, {
|
||||
operation: "sign_in_reconnect_error",
|
||||
interaction: name,
|
||||
userId,
|
||||
hostId: completed.hostId,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message:
|
||||
"Failed to connect after authentication: " +
|
||||
getErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleConnectToHost(data: ConnectToHostData) {
|
||||
const { hostConfig, initialPath, executeCommand, tmuxAttachSession } =
|
||||
data;
|
||||
@@ -2360,7 +2292,7 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
port,
|
||||
username,
|
||||
authType: resolvedCredentials.authType,
|
||||
warpgateAuthPromptSent,
|
||||
browserSignInId,
|
||||
isKeyboardInteractive,
|
||||
hasKeyboardInteractiveFinish: !!keyboardInteractiveFinish,
|
||||
keyboardInteractiveResponded,
|
||||
@@ -2680,9 +2612,9 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
keyboardInteractiveResponded,
|
||||
keyboardInteractiveFinish,
|
||||
totpPromptSent,
|
||||
warpgateAuthPromptSent,
|
||||
browserSignInId,
|
||||
totpTimeout,
|
||||
warpgateAuthTimeout,
|
||||
browserSignInTimeout,
|
||||
totpAttempts: 0,
|
||||
});
|
||||
|
||||
@@ -2709,7 +2641,9 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
resolvedCredentials as unknown as Parameters<
|
||||
typeof sshAuthManager.handleKeyboardInteractive
|
||||
>[5],
|
||||
hostConfig,
|
||||
// The resolved host carries the plugin host settings that
|
||||
// keyboard-interactive handlers read.
|
||||
connectTarget ?? hostConfig,
|
||||
);
|
||||
|
||||
isKeyboardInteractive =
|
||||
@@ -2719,10 +2653,9 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
keyboardInteractiveFinish =
|
||||
sshAuthManager.context.keyboardInteractiveFinish;
|
||||
totpPromptSent = sshAuthManager.context.totpPromptSent;
|
||||
warpgateAuthPromptSent =
|
||||
sshAuthManager.context.warpgateAuthPromptSent;
|
||||
browserSignInId = sshAuthManager.context.browserSignInId;
|
||||
totpTimeout = sshAuthManager.context.totpTimeout;
|
||||
warpgateAuthTimeout = sshAuthManager.context.warpgateAuthTimeout;
|
||||
browserSignInTimeout = sshAuthManager.context.browserSignInTimeout;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -2759,7 +2692,6 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
keyType: resolvedCredentials.keyType,
|
||||
certPublicKey: resolvedCredentials.certPublicKey,
|
||||
forceKeyboardInteractive: hostConfig.forceKeyboardInteractive,
|
||||
useWarpgate: hostConfig.useWarpgate,
|
||||
terminalConfig: hostConfig.terminalConfig,
|
||||
jumpHosts: hostConfig.jumpHosts,
|
||||
useSocks5: hostConfig.useSocks5,
|
||||
@@ -2946,9 +2878,9 @@ export function createTerminalSocket(deps: TerminalSocketDeps) {
|
||||
totpTimeout = null;
|
||||
}
|
||||
|
||||
if (warpgateAuthTimeout) {
|
||||
clearTimeout(warpgateAuthTimeout);
|
||||
warpgateAuthTimeout = null;
|
||||
if (browserSignInTimeout) {
|
||||
clearTimeout(browserSignInTimeout);
|
||||
browserSignInTimeout = null;
|
||||
}
|
||||
|
||||
sshStream = null;
|
||||
|
||||
@@ -88,8 +88,7 @@ import {
|
||||
TOTPDialog,
|
||||
SSHAuthDialog,
|
||||
PassphraseDialog,
|
||||
WarpgateDialog,
|
||||
OPKSSHDialog,
|
||||
BrowserSignInDialog,
|
||||
HostKeyVerificationDialog,
|
||||
DEFAULT_TERMINAL_CONFIG,
|
||||
TERMINAL_FONTS,
|
||||
@@ -314,22 +313,15 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
>("no_keyboard");
|
||||
const [showPassphraseDialog, setShowPassphraseDialog] = useState(false);
|
||||
const [, setKeyboardInteractiveDetected] = useState(false);
|
||||
const [warpgateAuthRequired, setWarpgateAuthRequired] = useState(false);
|
||||
const [warpgateAuthUrl, setWarpgateAuthUrl] = useState<string>("");
|
||||
const [warpgateSecurityKey, setWarpgateSecurityKey] = useState<string>("");
|
||||
const warpgateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [opksshDialog, setOpksshDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
authUrl: string;
|
||||
requestId: string;
|
||||
stage: "chooser" | "waiting" | "authenticating" | "completed" | "error";
|
||||
error?: string;
|
||||
providers?: Array<{ alias: string; issuer: string }>;
|
||||
/** Which issuer is asking (OPKSSH by default, "Step CA", ...). */
|
||||
label?: string;
|
||||
// A keyboard-interactive round finished in a browser, named after the
|
||||
// handler that claimed it (see "<id>_auth_required").
|
||||
const [browserSignIn, setBrowserSignIn] = useState<{
|
||||
id: string;
|
||||
label: string;
|
||||
url: string;
|
||||
code: string;
|
||||
} | null>(null);
|
||||
const opksshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const browserSignInTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Overlays contributed by plugins (a Tailscale check, say) hear the
|
||||
// session's messages and may hold the connect timeout while they wait.
|
||||
@@ -347,7 +339,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const opksshFailedRef = useRef(false);
|
||||
const currentHostIdRef = useRef<number | null>(null);
|
||||
const currentHostConfigRef = useRef<TerminalHostConfig | null>(null);
|
||||
|
||||
@@ -839,8 +830,19 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
webSocketRef.current?.close();
|
||||
},
|
||||
disconnect: () => webSocketRef.current?.close(),
|
||||
send: (type, data) => {
|
||||
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
||||
webSocketRef.current.send(JSON.stringify({ type, data }));
|
||||
}
|
||||
},
|
||||
connectPayload: () => ({
|
||||
hostId: currentHostIdRef.current,
|
||||
cols: terminal?.cols || 80,
|
||||
rows: terminal?.rows || 24,
|
||||
hostConfig: currentHostConfigRef.current,
|
||||
}),
|
||||
}),
|
||||
[host, backgroundColor, updateConnectionError],
|
||||
[host, backgroundColor, updateConnectionError, terminal],
|
||||
);
|
||||
|
||||
const activityLoggingRef = useRef(false);
|
||||
@@ -1087,38 +1089,33 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
if (onClose) onClose();
|
||||
}
|
||||
|
||||
function handleWarpgateContinue() {
|
||||
if (webSocketRef.current) {
|
||||
if (warpgateTimeoutRef.current) {
|
||||
clearTimeout(warpgateTimeoutRef.current);
|
||||
warpgateTimeoutRef.current = null;
|
||||
}
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "warpgate_auth_continue",
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
setWarpgateAuthRequired(false);
|
||||
setWarpgateAuthUrl("");
|
||||
setWarpgateSecurityKey("");
|
||||
function clearBrowserSignIn() {
|
||||
if (browserSignInTimeoutRef.current) {
|
||||
clearTimeout(browserSignInTimeoutRef.current);
|
||||
browserSignInTimeoutRef.current = null;
|
||||
}
|
||||
setBrowserSignIn(null);
|
||||
}
|
||||
|
||||
function handleWarpgateCancel() {
|
||||
if (warpgateTimeoutRef.current) {
|
||||
clearTimeout(warpgateTimeoutRef.current);
|
||||
warpgateTimeoutRef.current = null;
|
||||
}
|
||||
setWarpgateAuthRequired(false);
|
||||
setWarpgateAuthUrl("");
|
||||
setWarpgateSecurityKey("");
|
||||
function handleBrowserSignInContinue() {
|
||||
if (!browserSignIn || !webSocketRef.current) return;
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: `${browserSignIn.id}_auth_continue`,
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
clearBrowserSignIn();
|
||||
}
|
||||
|
||||
function handleBrowserSignInCancel() {
|
||||
clearBrowserSignIn();
|
||||
if (onClose) onClose();
|
||||
}
|
||||
|
||||
function handleWarpgateOpenUrl() {
|
||||
if (warpgateAuthUrl) {
|
||||
window.open(warpgateAuthUrl, "_blank", "noopener,noreferrer");
|
||||
function handleBrowserSignInOpenUrl() {
|
||||
if (browserSignIn?.url) {
|
||||
window.open(browserSignIn.url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1356,9 +1353,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
clearTimeout(totpTimeoutRef.current);
|
||||
totpTimeoutRef.current = null;
|
||||
}
|
||||
if (warpgateTimeoutRef.current) {
|
||||
clearTimeout(warpgateTimeoutRef.current);
|
||||
warpgateTimeoutRef.current = null;
|
||||
if (browserSignInTimeoutRef.current) {
|
||||
clearTimeout(browserSignInTimeoutRef.current);
|
||||
browserSignInTimeoutRef.current = null;
|
||||
}
|
||||
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
||||
webSocketRef.current.send(JSON.stringify({ type: "disconnect" }));
|
||||
@@ -1912,7 +1909,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
updateConnectionError(errorMessage);
|
||||
setIsConnecting(false);
|
||||
} else if (msg.type === "connected") {
|
||||
opksshFailedRef.current = false;
|
||||
vaultFailedRef.current = false;
|
||||
wasConnectedRef.current = true;
|
||||
setIsConnected(true);
|
||||
@@ -2084,51 +2080,30 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
},
|
||||
isPush ? 300000 : 180000,
|
||||
);
|
||||
} else if (msg.type === "warpgate_auth_required") {
|
||||
setWarpgateAuthRequired(true);
|
||||
setWarpgateAuthUrl(msg.url || "");
|
||||
setWarpgateSecurityKey(msg.securityKey || "N/A");
|
||||
} else if (
|
||||
msg.kind === "browser" &&
|
||||
typeof msg.type === "string" &&
|
||||
msg.type.endsWith("_auth_required")
|
||||
) {
|
||||
setBrowserSignIn({
|
||||
id: msg.type.slice(0, -"_auth_required".length),
|
||||
label: typeof msg.label === "string" ? msg.label : "",
|
||||
url: msg.url || "",
|
||||
code: msg.securityKey || "",
|
||||
});
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
if (warpgateTimeoutRef.current) {
|
||||
clearTimeout(warpgateTimeoutRef.current);
|
||||
if (browserSignInTimeoutRef.current) {
|
||||
clearTimeout(browserSignInTimeoutRef.current);
|
||||
}
|
||||
warpgateTimeoutRef.current = setTimeout(() => {
|
||||
setWarpgateAuthRequired(false);
|
||||
browserSignInTimeoutRef.current = setTimeout(() => {
|
||||
setBrowserSignIn(null);
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
}, 300000);
|
||||
} else if (msg.type === "opkssh_auth_required") {
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
if (opksshFailedRef.current) {
|
||||
setOpksshDialog(null);
|
||||
if (opksshTimeoutRef.current) {
|
||||
clearTimeout(opksshTimeoutRef.current);
|
||||
opksshTimeoutRef.current = null;
|
||||
}
|
||||
updateConnectionError(t("terminal.opksshAuthFailed"));
|
||||
addLog({
|
||||
type: "error",
|
||||
stage: "auth",
|
||||
message: t("terminal.opksshAuthFailed"),
|
||||
});
|
||||
} else {
|
||||
opksshFailedRef.current = true;
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "opkssh_start_auth",
|
||||
data: { hostId: msg.hostId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "vault_auth_required") {
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
@@ -2209,98 +2184,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
setVaultDialog({ stage: "error", error: msg.error });
|
||||
setIsConnecting(false);
|
||||
} else if (msg.type === "opkssh_status") {
|
||||
if (connectionErrorRef.current) return;
|
||||
if (msg.stage === "chooser") {
|
||||
setOpksshDialog({
|
||||
isOpen: true,
|
||||
authUrl: msg.url || "",
|
||||
requestId: msg.requestId || "",
|
||||
stage: "chooser",
|
||||
providers: msg.providers,
|
||||
label: typeof msg.label === "string" ? msg.label : undefined,
|
||||
});
|
||||
if (opksshTimeoutRef.current) {
|
||||
clearTimeout(opksshTimeoutRef.current);
|
||||
}
|
||||
opksshTimeoutRef.current = setTimeout(() => {
|
||||
setOpksshDialog(null);
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
}, 300000);
|
||||
} else {
|
||||
setOpksshDialog((prev) =>
|
||||
prev ? { ...prev, stage: msg.stage } : null,
|
||||
);
|
||||
}
|
||||
} else if (msg.type === "opkssh_completed") {
|
||||
if (opksshTimeoutRef.current) {
|
||||
clearTimeout(opksshTimeoutRef.current);
|
||||
opksshTimeoutRef.current = null;
|
||||
}
|
||||
setOpksshDialog(null);
|
||||
if (webSocketRef.current && terminal) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "opkssh_auth_completed",
|
||||
data: {
|
||||
hostId: currentHostIdRef.current,
|
||||
cols: terminal.cols || 80,
|
||||
rows: terminal.rows || 24,
|
||||
hostConfig: currentHostConfigRef.current,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (msg.type === "opkssh_error") {
|
||||
if (connectionErrorRef.current) return;
|
||||
opksshFailedRef.current = true;
|
||||
if (opksshDialog) {
|
||||
setOpksshDialog((prev) =>
|
||||
prev ? { ...prev, stage: "error", error: msg.error } : null,
|
||||
);
|
||||
} else {
|
||||
setOpksshDialog({
|
||||
isOpen: true,
|
||||
authUrl: "",
|
||||
requestId: msg.requestId || "",
|
||||
stage: "error",
|
||||
error: msg.error,
|
||||
});
|
||||
}
|
||||
setIsConnecting(false);
|
||||
} else if (msg.type === "opkssh_timeout") {
|
||||
if (connectionErrorRef.current) return;
|
||||
opksshFailedRef.current = true;
|
||||
if (opksshDialog) {
|
||||
setOpksshDialog((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
stage: "error",
|
||||
error: t("terminal.opksshTimeout"),
|
||||
}
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
setOpksshDialog({
|
||||
isOpen: true,
|
||||
authUrl: "",
|
||||
requestId: msg.requestId || "",
|
||||
stage: "error",
|
||||
error: t("terminal.opksshTimeout"),
|
||||
});
|
||||
}
|
||||
setIsConnecting(false);
|
||||
} else if (msg.type === "opkssh_config_error") {
|
||||
setOpksshDialog({
|
||||
isOpen: true,
|
||||
authUrl: "",
|
||||
requestId: msg.requestId || "",
|
||||
stage: "error",
|
||||
error: msg.instructions || msg.error,
|
||||
});
|
||||
} else if (msg.type === "keyboard_interactive_available") {
|
||||
setKeyboardInteractiveDetected(true);
|
||||
setIsConnecting(false);
|
||||
@@ -2357,7 +2240,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
} else if (msg.type === "sessionAttached") {
|
||||
isAttachingSessionRef.current = false;
|
||||
opksshFailedRef.current = false;
|
||||
vaultFailedRef.current = false;
|
||||
wasConnectedRef.current = true;
|
||||
setIsConnected(true);
|
||||
@@ -3985,71 +3867,17 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
|
||||
<WarpgateDialog
|
||||
isOpen={warpgateAuthRequired}
|
||||
url={warpgateAuthUrl}
|
||||
securityKey={warpgateSecurityKey}
|
||||
onContinue={handleWarpgateContinue}
|
||||
onCancel={handleWarpgateCancel}
|
||||
onOpenUrl={handleWarpgateOpenUrl}
|
||||
<BrowserSignInDialog
|
||||
isOpen={browserSignIn !== null}
|
||||
label={browserSignIn?.label ?? ""}
|
||||
url={browserSignIn?.url ?? ""}
|
||||
code={browserSignIn?.code ?? ""}
|
||||
onContinue={handleBrowserSignInContinue}
|
||||
onCancel={handleBrowserSignInCancel}
|
||||
onOpenUrl={handleBrowserSignInOpenUrl}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
|
||||
{opksshDialog?.isOpen && (
|
||||
<OPKSSHDialog
|
||||
isOpen={opksshDialog.isOpen}
|
||||
authUrl={opksshDialog.authUrl}
|
||||
requestId={opksshDialog.requestId}
|
||||
stage={opksshDialog.stage}
|
||||
error={opksshDialog.error}
|
||||
providers={opksshDialog.providers}
|
||||
label={opksshDialog.label}
|
||||
onCancel={() => {
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "opkssh_cancel",
|
||||
data: { requestId: opksshDialog.requestId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
setOpksshDialog(null);
|
||||
if (opksshTimeoutRef.current) {
|
||||
clearTimeout(opksshTimeoutRef.current);
|
||||
opksshTimeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
onOpenUrl={() => {
|
||||
window.open(opksshDialog.authUrl, "_blank");
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "opkssh_browser_opened",
|
||||
data: { requestId: opksshDialog.requestId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
onSelectProvider={(alias) => {
|
||||
if (!opksshDialog.authUrl) return;
|
||||
const selectUrl = `${opksshDialog.authUrl}/select?op=${encodeURIComponent(alias)}`;
|
||||
window.open(selectUrl, "_blank");
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "opkssh_browser_opened",
|
||||
data: { requestId: opksshDialog.requestId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
setOpksshDialog((prev) =>
|
||||
prev ? { ...prev, stage: "waiting" } : null,
|
||||
);
|
||||
}}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ComponentSlot
|
||||
slotId={TERMINAL_OVERLAY_SLOT}
|
||||
when={{ host }}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Host } from "../types";
|
||||
* `openSidePanel(id)`, rendered with TerminalSidePanelProps.
|
||||
* - "terminal.overlay" (components): always mounted over the terminal and
|
||||
* given the session's server messages, for connection flows a plugin owns
|
||||
* (a Tailscale check, say).
|
||||
* (a Tailscale check, an OPKSSH sign-in). It can answer on the socket.
|
||||
*
|
||||
* Every contribution's `when` sees `{ host }`.
|
||||
*/
|
||||
@@ -79,4 +79,11 @@ export interface TerminalOverlayProps {
|
||||
fail: (message: string) => void;
|
||||
/** Closes the session's connection. */
|
||||
disconnect: () => void;
|
||||
/** Sends a message on the session's socket, as { type, data }. */
|
||||
send: (type: string, data?: unknown) => void;
|
||||
/**
|
||||
* What the server needs to connect again after a sign-in finished: send it
|
||||
* as the data of "<interaction>_auth_completed".
|
||||
*/
|
||||
connectPayload: () => Record<string, unknown>;
|
||||
}
|
||||
@@ -27,11 +27,14 @@ function classify(
|
||||
},
|
||||
host: PluginSshHost,
|
||||
): PluginKeyboardInteractiveDecision {
|
||||
if (/warpgate/i.test(round.name)) {
|
||||
// Stands in for a plugin's keyboard-interactive handler.
|
||||
if (/gateway sign-in/i.test(round.name)) {
|
||||
return {
|
||||
kind: "warpgate",
|
||||
kind: "browser",
|
||||
id: "gateway",
|
||||
label: "Gateway",
|
||||
url: round.instructions.match(/https?:\/\/\S+/)?.[0] ?? null,
|
||||
securityKey: round.instructions.match(/Security key: (\S+)/)?.[1] ?? "",
|
||||
code: round.instructions.match(/Security key: (\S+)/)?.[1] ?? "",
|
||||
instructions: round.instructions,
|
||||
};
|
||||
}
|
||||
@@ -72,9 +75,9 @@ function createManager() {
|
||||
keyboardInteractiveResponded: false,
|
||||
keyboardInteractiveFinish: null,
|
||||
totpPromptSent: false,
|
||||
warpgateAuthPromptSent: false,
|
||||
browserSignInId: null,
|
||||
totpTimeout: null,
|
||||
warpgateAuthTimeout: null,
|
||||
browserSignInTimeout: null,
|
||||
totpAttempts: 0,
|
||||
});
|
||||
return { manager, sent };
|
||||
@@ -258,13 +261,13 @@ describe("SSHAuthManager.handleKeyboardInteractive", () => {
|
||||
expect(finish).toHaveBeenCalledWith(["push"]);
|
||||
});
|
||||
|
||||
it("routes Warpgate prompts to the warpgate flow, not the generic path", () => {
|
||||
it("sends a claimed browser round under its handler's name", () => {
|
||||
const { manager, sent } = createManager();
|
||||
const finish = vi.fn();
|
||||
|
||||
manager.handleKeyboardInteractive(
|
||||
"Warpgate Authentication",
|
||||
"Visit https://warpgate.example.com/auth to continue. Security key: AB12",
|
||||
"Gateway sign-in",
|
||||
"Visit https://gateway.example.com/auth to continue. Security key: AB12",
|
||||
"",
|
||||
[{ prompt: "Press enter once done: ", echo: true }],
|
||||
finish,
|
||||
@@ -277,16 +280,19 @@ describe("SSHAuthManager.handleKeyboardInteractive", () => {
|
||||
data: {
|
||||
stage: "auth",
|
||||
level: "info",
|
||||
message: "Warpgate authentication required",
|
||||
message: "Gateway sign-in required",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "warpgate_auth_required",
|
||||
url: "https://warpgate.example.com/auth",
|
||||
type: "gateway_auth_required",
|
||||
kind: "browser",
|
||||
label: "Gateway",
|
||||
url: "https://gateway.example.com/auth",
|
||||
securityKey: "AB12",
|
||||
instructions:
|
||||
"Visit https://warpgate.example.com/auth to continue. Security key: AB12",
|
||||
"Visit https://gateway.example.com/auth to continue. Security key: AB12",
|
||||
},
|
||||
]);
|
||||
expect(manager.context.browserSignInId).toBe("gateway");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user