diff --git a/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java b/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java index fbdb516d3..00b9f5176 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java @@ -87,7 +87,7 @@ public class OptionsComp extends Comp> { if (entry.description() != null) { var description = new Label(); - description.wrapTextProperty().bind(pane.visibleProperty()); + description.setWrapText(true); description.getStyleClass().add("description"); description.textProperty().bind(entry.description()); description.setAlignment(Pos.CENTER_LEFT); diff --git a/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java index 5f084fb1f..7816fa88f 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/AppOperationMode.java @@ -24,6 +24,7 @@ import javafx.application.Platform; import lombok.Getter; import lombok.SneakyThrows; +import java.awt.*; import java.time.Duration; import java.util.List; @@ -90,6 +91,11 @@ public abstract class AppOperationMode { return; } + // Some random AWT errors are thrown sometimes + if (ex instanceof AWTError) { + return; + } + // Handle any startup uncaught errors if (AppOperationMode.isInStartup() && thread.threadId() == 1) { ex.printStackTrace(); diff --git a/app/src/main/java/io/xpipe/app/platform/PlatformState.java b/app/src/main/java/io/xpipe/app/platform/PlatformState.java index 28faafbd2..65034da67 100644 --- a/app/src/main/java/io/xpipe/app/platform/PlatformState.java +++ b/app/src/main/java/io/xpipe/app/platform/PlatformState.java @@ -150,6 +150,7 @@ public enum PlatformState { // Platform initialization has failed in this case var msg = getErrorMessage(t.getMessage()); var ex = new UnsupportedOperationException(msg, t); + ErrorEventFactory.expected(ex); PlatformState.setCurrent(PlatformState.EXITED); lastError = ex; return; diff --git a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java index 82b5022ad..ba8a8eb9a 100644 --- a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java @@ -22,11 +22,13 @@ import org.kordamp.ikonli.javafx.FontIcon; import java.time.Duration; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class PasswordManagerTestComp extends SimpleComp { private final StringProperty value; private final boolean handleEnter; + private final AtomicInteger counter = new AtomicInteger(0); public PasswordManagerTestComp(StringProperty value, boolean handleEnter) { this.value = value; @@ -85,6 +87,7 @@ public class PasswordManagerTestComp extends SimpleComp { } private void testPasswordManager(String key, StringProperty testPasswordManagerResult) { + var currentIndex = counter.incrementAndGet(); var prefs = AppPrefs.get(); ThreadHelper.runFailableAsync(() -> { if (prefs.passwordManager.getValue() == null || key == null) { @@ -111,7 +114,9 @@ public class PasswordManagerTestComp extends SimpleComp { GlobalTimer.delay( () -> { Platform.runLater(() -> { - testPasswordManagerResult.set(null); + if (counter.get() == currentIndex) { + testPasswordManagerResult.set(null); + } }); }, Duration.ofSeconds(5)); diff --git a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java index f6002d7f1..0f4aa2c71 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java @@ -1,26 +1,50 @@ package io.xpipe.app.pwman; +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.comp.base.ListBoxViewComp; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.platform.DerivedObservableList; +import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.process.*; import io.xpipe.app.secret.SecretManager; import io.xpipe.app.secret.SecretPromptStrategy; +import io.xpipe.app.secret.SecretQueryState; import io.xpipe.app.terminal.TerminalLaunch; -import io.xpipe.core.InPlaceSecretValue; -import io.xpipe.core.JacksonMapper; -import io.xpipe.core.OsType; +import io.xpipe.app.util.AskpassAlert; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.*; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.jackson.Jacksonized; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; import java.util.UUID; @JsonTypeName("keeper") +@Getter +@Builder(toBuilder = true) +@ToString +@Jacksonized public class KeeperPasswordManager implements PasswordManager { private static final UUID KEEPER_PASSWORD_ID = UUID.randomUUID(); private static ShellControl SHELL; + private final Boolean mfa; private static synchronized ShellControl getOrStartShell() throws Exception { if (SHELL == null) { @@ -31,18 +55,29 @@ public class KeeperPasswordManager implements PasswordManager { } private String getExecutable(ShellControl sc) { - return sc.getShellDialect() == ShellDialects.CMD - ? "@keeper" - : (OsType.ofLocal() == OsType.WINDOWS ? "keeper-commander" : "keeper"); + return OsType.ofLocal() == OsType.WINDOWS ? "keeper-commander" : "keeper"; + } + + @SuppressWarnings("unused") + public static OptionsBuilder createOptions(Property p) { + var mfa = new SimpleObjectProperty<>(p.getValue().getMfa()); + return new OptionsBuilder() + .nameAndDescription("keeperUseMfa") + .addToggle(mfa) + .bind( + () -> { + return KeeperPasswordManager.builder().mfa(mfa.get()).build(); + }, + p); } @Override public synchronized CredentialResult retrieveCredentials(String key) { // The copy UID button copies the whole URL in the Keeper UI. Why? ... - key = key.replaceFirst("https://keepersecurity\\.\\w+/vault/#detail/", ""); + key = key.replaceFirst("https://\\w+\\.\\w+/vault/#detail/", ""); try { - CommandSupport.isInLocalPathOrThrow("Keeper Commander CLI", "keeper"); + CommandSupport.isInLocalPathOrThrow("Keeper Commander CLI", "keeper-commander"); } catch (Exception e) { ErrorEventFactory.fromThrowable(e) .link("https://docs.keeper.io/en/keeperpam/commander-cli/commander-installation-setup") @@ -52,8 +87,8 @@ public class KeeperPasswordManager implements PasswordManager { try { var sc = getOrStartShell(); - var file = sc.view().userHome().join(".keeper", "config.json"); - if (!sc.view().fileExists(file)) { + var config = sc.view().userHome().join(".keeper", "config.json"); + if (!sc.view().fileExists(config)) { var script = ShellScript.lines( sc.getShellDialect().getEchoCommand("Log in into your Keeper account from the CLI:", false), getExecutable(sc) + " login"); @@ -76,27 +111,125 @@ public class KeeperPasswordManager implements PasswordManager { return null; } - var out = sc.command(CommandBuilder.of() - .add(getExecutable(sc), "get") - .addLiteral(key) - .add("--format", "json", "--unmask") - .add("--password") - .addLiteral(r.getSecretValue())) - .sensitive() - .readStdoutOrThrow(); - JsonNode tree; - try { - tree = JacksonMapper.getDefault().readTree(out); - } catch (JsonProcessingException e) { - ErrorEventFactory.fromMessage(out).expected().handle(); + if (r.getSecretValue().contains("\"")) { + SecretManager.clearAll(KEEPER_PASSWORD_ID); + throw ErrorEventFactory.expected(new IllegalArgumentException("Keeper password contains double quote \" character, which is not supported by the Keeper Commander application")); + } + + var b = CommandBuilder.of() + .add(getExecutable(sc), "get") + .addLiteral(key) + .add("--format", "json", "--unmask") + .add("--password") + .addLiteral(r.getSecretValue()); + FilePath file = null; + CommandBuilder fullB; + if (mfa != null && mfa) { + var totp = AskpassAlert.queryRaw("Enter Keeper 2FA Code", null, true); + if (totp.getState() != SecretQueryState.NORMAL) { + return null; + } + + var input = """ + + 1 + %s + """.formatted(totp.getSecret().getSecretValue()); + file = sc.getSystemTemporaryDirectory().join("keeper.txt"); + sc.view().writeTextFile(file, input); + fullB = CommandBuilder.of().add(sc.getShellDialect() == ShellDialects.CMD ? "type" : "cat").addFile(file).add("|").add(b); + } else { + fullB = b; + } + + var queryCommand = sc.command(fullB); + queryCommand.sensitive(); + queryCommand.killOnTimeout(CountDown.of().start(15_000)); + + var result = queryCommand.readStdoutAndStderr(); + var exitCode = queryCommand.getExitCode(); + + if (file != null) { + sc.view().deleteFileIfPossible(file); + } + + var out = result[0].replace("\r\n", "\n").replace(""" + Selection: Invalid entry, additional factors of authentication shown may be configured if not currently enabled. + Selection:\s + 2FA Code Duration: Require Every Login. + To change duration: 2fa_duration=login|12_hours|24_hours|30_days|forever + """, "") + .replace(""" + This account requires 2FA Authentication + + 1. TOTP (Google and Microsoft Authenticator) \s + q. Quit login attempt and return to Commander prompt + """, "") + .replace("Selection:", "") + .strip(); + var err = result[1].replace("\r\n", "\n") + .replace(""" + EOF when reading a line + """, "") + .strip(); + + var jsonStart = out.indexOf("{\n"); + var jsonEnd = out.indexOf("\n}"); + if (jsonEnd != -1) { + jsonEnd += 2; + } + + var outPrefix = jsonStart <= 0 ? out : out.substring(0, jsonStart); + var outJson = jsonStart <= 0 ? (jsonEnd != -1 ? out.substring(0, jsonEnd) : out) : + (jsonEnd != -1 ? out.substring(jsonStart, jsonEnd) : out.substring(jsonStart)); + + if (exitCode != 0) { + var wrongPw = outPrefix.contains("Enter password for"); + if (wrongPw) { + SecretManager.clearAll(KEEPER_PASSWORD_ID); + ErrorEventFactory.fromMessage("Master password was not accepted by Keeper. Is it correct?").expected().handle(); + return null; + } + + var message = !err.isEmpty() ? outPrefix + "\n" + err : outPrefix; + ErrorEventFactory.fromMessage(message).expected().handle(); return null; } - var fields = tree.required("fields"); - if (!fields.isArray()) { + JsonNode tree; + try { + tree = JacksonMapper.getDefault().readTree(outJson); + } catch (JsonProcessingException e) { + var message = !err.isEmpty() ? outPrefix + "\n" + err : outPrefix; + ErrorEventFactory.fromMessage(message).expected().handle(); return null; } + var fields = tree.get("fields"); + // There multiple schemas + if (fields == null || !fields.isArray()) { + String login = null; + String password = null; + + var l = tree.get("login"); + if (l != null && l.isTextual()) { + login = l.asText(); + } + + var p = tree.get("password"); + if (p != null && p.isTextual()) { + password = p.asText(); + } + + if (login == null && password == null) { + var message = !err.isEmpty() ? out + "\n" + err : out; + ErrorEventFactory.fromMessage(message).description("Received invalid response").expected().handle(); + return null; + } + + return new CredentialResult(login, password != null ? InPlaceSecretValue.of(password) : null); + } + String login = null; String password = null; for (JsonNode field : fields) { diff --git a/app/src/main/java/io/xpipe/app/secret/SecretPasswordManagerStrategy.java b/app/src/main/java/io/xpipe/app/secret/SecretPasswordManagerStrategy.java index 908b5ea86..bd2f13464 100644 --- a/app/src/main/java/io/xpipe/app/secret/SecretPasswordManagerStrategy.java +++ b/app/src/main/java/io/xpipe/app/secret/SecretPasswordManagerStrategy.java @@ -105,7 +105,7 @@ public class SecretPasswordManagerStrategy implements SecretRetrievalStrategy { @Override public Duration cacheDuration() { // To reduce password manager access, cache it for a few seconds - return Duration.ofSeconds(10); + return Duration.ofSeconds(15); } @Override diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java index a5a4adbf2..e324081f4 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java @@ -169,37 +169,30 @@ public class TerminalLauncher { preferTabs && AppPrefs.get().preferTerminalTabs().get(); var launchConfig = new TerminalLaunchConfiguration(color, adjustedTitle, cleanTitle, preferTabs, paneList); - // Used for multiplexers and proxies - var launchRequest = UUID.randomUUID(); - if (effectivePreferTabs) { synchronized (TerminalLauncher.class) { // There will be timing issues when launching multiple tabs in a short time span TerminalMultiplexerManager.synchronizeMultiplexerLaunchTiming(); - // Let multiplexer know we launched something, even if there is no multiplexer - // This is to be prepared for settings changes later on where the multiplexer used is changed - TerminalMultiplexerManager.registerSessionLaunch(launchRequest, launchConfig); + // Track sessions that are used for the multiplexer + // Used to figure out when it dies + TerminalMultiplexerManager.registerSessionLaunch(launchConfig); if (launchMultiplexerTabInExistingTerminal(launchConfig)) { latch.await(); return; } - var multiplexerConfig = launchMultiplexerTabInNewTerminal(launchConfig, launchRequest); + var multiplexerConfig = launchMultiplexerTabInNewTerminal(launchConfig); if (multiplexerConfig.isPresent()) { - // Use first tab to track when multiplexer has started up - TerminalMultiplexerManager.registerMultiplexerLaunch( - paneList.getFirst().getRequest()); launch(type, multiplexerConfig.get(), latch); return; } } } - var proxyConfig = launchProxy(launchConfig, launchRequest); + var proxyConfig = launchProxy(launchConfig); if (proxyConfig.isPresent()) { - TerminalProxyManager.registerSessionLaunch(launchRequest, launchConfig); launch(type, proxyConfig.get(), latch); return; } @@ -246,17 +239,28 @@ public class TerminalLauncher { return false; } - var multiplexerCommand = multiplexer + // Map panes to original multiplexer request session + var mapped = TerminalMultiplexerManager.getActiveMultiplexerContainerRequest(); + if (mapped.isPresent()) { + for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) { + TerminalView.get().addSubstitution(pane.getRequest(), mapped.get()); + } + } + + var multiplexerCommandString = multiplexer .get() .launchForExistingSession(control, launchConfiguration) .toString(); - control.command(multiplexerCommand).execute(); + CommandControl multiplexerCommand = control.command(multiplexerCommandString); + // Multiplexer might freeze + multiplexerCommand.killOnTimeout(CountDown.of().start(10000)); + multiplexerCommand.execute(); TerminalView.focus(session.get()); return true; } private static Optional launchMultiplexerTabInNewTerminal( - TerminalLaunchConfiguration launchConfiguration, UUID launchRequest) throws Exception { + TerminalLaunchConfiguration launchConfiguration) throws Exception { var multiplexer = TerminalMultiplexerManager.getEffectiveMultiplexer(); if (multiplexer.isEmpty()) { return Optional.empty(); @@ -265,11 +269,19 @@ public class TerminalLauncher { // Throw if not supported multiplexer.get().checkSupported(TerminalProxyManager.getProxy().orElse(LocalShell.getShell())); - if (TerminalMultiplexerManager.getActiveMultiplexerSession().isPresent()) { + if (TerminalMultiplexerManager.getActiveMultiplexerContainerRequest().isPresent()) { return Optional.empty(); } - var multiplexerLaunchRequest = UUID.randomUUID(); + var multiplexerContainerLaunchRequest = UUID.randomUUID(); + // Map panes to original multiplexer request session + for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) { + TerminalView.get().addSubstitution(pane.getRequest(), multiplexerContainerLaunchRequest); + } + // Use initial shell session to track when multiplexer has started up + TerminalMultiplexerManager.registerMultiplexerContainerLaunch(multiplexerContainerLaunchRequest); + + var multiplexerTabLaunchRequest = UUID.randomUUID(); var proxyControl = TerminalProxyManager.getProxy(); if (proxyControl.isPresent()) { @@ -286,9 +298,9 @@ public class TerminalLauncher { // Restart for the next time proxyControl.get().start(); var fullLocalCommand = - getTerminalRegisterCommand(launchRequest, LocalShell.getShell()) + "\n" + proxyLaunchCommand; + getTerminalRegisterCommand(multiplexerContainerLaunchRequest, LocalShell.getShell()) + "\n" + proxyLaunchCommand; var pane = new TerminalPaneConfiguration( - multiplexerLaunchRequest, + multiplexerTabLaunchRequest, AppNames.ofCurrent().getName(), 0, fullLocalCommand, @@ -306,9 +318,9 @@ public class TerminalLauncher { TerminalInitScriptConfig.ofName(AppNames.ofCurrent().getName()), WorkingDirectoryFunction.none()); var fullLocalCommand = - getTerminalRegisterCommand(launchRequest, LocalShell.getShell()) + "\n" + launchCommand; + getTerminalRegisterCommand(multiplexerContainerLaunchRequest, LocalShell.getShell()) + "\n" + launchCommand; var pane = new TerminalPaneConfiguration( - multiplexerLaunchRequest, + multiplexerTabLaunchRequest, AppNames.ofCurrent().getName(), 0, fullLocalCommand, @@ -319,12 +331,18 @@ public class TerminalLauncher { } private static Optional launchProxy( - TerminalLaunchConfiguration launchConfiguration, UUID launchRequest) throws Exception { + TerminalLaunchConfiguration launchConfiguration) throws Exception { var proxyControl = TerminalProxyManager.getProxy(); if (proxyControl.isEmpty()) { return Optional.empty(); } + // We can't track sessions inside another environment, so map the ids to the proxy container process + var proxyContainerLaunchRequest = UUID.randomUUID(); + for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) { + TerminalView.get().addSubstitution(pane.getRequest(), proxyContainerLaunchRequest); + } + var panes = new ArrayList(); for (TerminalPaneConfiguration pane : launchConfiguration.getPanes()) { var openCommand = pane.getDialectLaunchCommand().buildSimple(); @@ -335,7 +353,7 @@ public class TerminalLauncher { TerminalInitScriptConfig.ofName(AppNames.ofCurrent().getName()), WorkingDirectoryFunction.none()); var fullLocalCommand = - getTerminalRegisterCommand(launchRequest, LocalShell.getShell()) + "\n" + launchCommand; + getTerminalRegisterCommand(proxyContainerLaunchRequest, LocalShell.getShell()) + "\n" + launchCommand; // Restart for the next time proxyControl.get().start(); panes.add(pane.withScript(LocalShell.getDialect(), fullLocalCommand)); diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalMultiplexerManager.java b/app/src/main/java/io/xpipe/app/terminal/TerminalMultiplexerManager.java index d2bd84ac9..ed38e6445 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalMultiplexerManager.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalMultiplexerManager.java @@ -13,14 +13,28 @@ public class TerminalMultiplexerManager { private static final Map connectionHubRequests = new HashMap<>(); private static UUID pendingMultiplexerLaunch; private static Instant lastCheck = Instant.now(); + private static UUID runningMultiplexerContainer; - public static void registerMultiplexerLaunch(UUID uuid) { + public static void registerMultiplexerContainerLaunch(UUID uuid) { pendingMultiplexerLaunch = uuid; var listener = new TerminalView.Listener() { @Override public void onSessionOpened(TerminalView.ShellSession session) { if (session.getRequest().equals(pendingMultiplexerLaunch)) { pendingMultiplexerLaunch = null; + runningMultiplexerContainer = uuid; + } + } + + @Override + public void onSessionClosed(TerminalView.ShellSession session) { + // Technically, due to how multiplexers handle, this can only be 0 or 1 + // as it only tracks the base shell session the multiplexer runs in + var left = TerminalView.get().getSessions().stream().filter(shellSession -> { + return connectionHubRequests.containsKey(shellSession.getRequest()) && shellSession.getTerminal().isRunning(); + }).count(); + if (left == 0) { + runningMultiplexerContainer = null; TerminalView.get().removeListener(this); } } @@ -73,7 +87,7 @@ public class TerminalMultiplexerManager { lastCheck = Instant.now(); } - public static void registerSessionLaunch(UUID launchRequestUuid, TerminalLaunchConfiguration configuration) { + public static void registerSessionLaunch(TerminalLaunchConfiguration configuration) { var mult = getEffectiveMultiplexer(); for (TerminalPaneConfiguration pane : configuration.getPanes()) { @@ -82,11 +96,14 @@ public class TerminalMultiplexerManager { return; } - TerminalView.get().addSubstitution(pane.getRequest(), launchRequestUuid); connectionHubRequests.put(pane.getRequest(), mult.orElse(null)); } } + public static Optional getActiveMultiplexerContainerRequest() { + return Optional.ofNullable(runningMultiplexerContainer); + } + public static Optional getActiveMultiplexerSession() { var mult = getEffectiveMultiplexer(); if (mult.isEmpty()) { diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalProxyManager.java b/app/src/main/java/io/xpipe/app/terminal/TerminalProxyManager.java index 4ed4f6e43..55457431b 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalProxyManager.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalProxyManager.java @@ -17,12 +17,6 @@ public class TerminalProxyManager { private static ActiveSession activeSession; - public static void registerSessionLaunch(UUID launchRequestUuid, TerminalLaunchConfiguration configuration) { - for (TerminalPaneConfiguration pane : configuration.getPanes()) { - TerminalView.get().addSubstitution(pane.getRequest(), launchRequestUuid); - } - } - public static boolean canUseAsProxy(DataStoreEntryRef ref) { if (!ref.get().getValidity().isUsable()) { return false; diff --git a/app/src/main/java/io/xpipe/app/terminal/ZellijTerminalMultiplexer.java b/app/src/main/java/io/xpipe/app/terminal/ZellijTerminalMultiplexer.java index 1a0adbdc2..4e1b4751c 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ZellijTerminalMultiplexer.java +++ b/app/src/main/java/io/xpipe/app/terminal/ZellijTerminalMultiplexer.java @@ -123,7 +123,10 @@ public class ZellijTerminalMultiplexer implements TerminalMultiplexer { TerminalView.get().removeListener(this); ThreadHelper.runFailableAsync(() -> { var sc = TerminalProxyManager.getProxy().orElse(LocalShell.getShell()); - sc.command(String.join("\n", asyncLines)).executeAndCheck(); + var command = sc.command(String.join("\n", asyncLines)); + // Zellij sometimes freezes + command.killOnTimeout(CountDown.of().start(10_000)); + command.executeAndCheck(); }); } }; diff --git a/app/src/main/java/io/xpipe/app/util/DocumentationLink.java b/app/src/main/java/io/xpipe/app/util/DocumentationLink.java index d3057e727..054c4035e 100644 --- a/app/src/main/java/io/xpipe/app/util/DocumentationLink.java +++ b/app/src/main/java/io/xpipe/app/util/DocumentationLink.java @@ -29,6 +29,8 @@ public enum DocumentationLink { KUBERNETES("guide/kubernetes"), DOCKER("guide/docker"), PROXMOX("guide/proxmox"), + PROXMOX_GUEST_AGENT("guide/proxmox#guest-agent"), + PROXMOX_NETWORKING("guide/proxmox#networking"), TAILSCALE("guide/tailscale"), TAILSCALE_AUTH("guide/tailscale#tailscale-authentication"), IDENTITY_APPLY("guide/ssh#applying-identities"), @@ -40,8 +42,11 @@ public enum DocumentationLink { PODMAN("guide/podman"), KVM("guide/kvm"), KVM_VNC("guide/kvm#vnc-access"), + KVM_GUEST_AGENT("guide/kvm#guest-agent"), + KVM_NETWORKING("guide/kvm#networking"), HCLOUD("guide/hcloud"), VMWARE("guide/vmware"), + VMWARE_NETWORKING("guide/vmware#networking"), AWS("guide/aws"), AWS_PROFILES("guide/aws#profiles"), AWS_EC2("guide/aws#ec2-instances"), @@ -78,6 +83,7 @@ public enum DocumentationLink { TUNNELS_REMOTE("guide/ssh#remote-tunnels"), TUNNELS_DYNAMIC("guide/ssh#dynamic-tunnels"), HYPERV("guide/hyperv"), + HYPERV_NETWORKING("guide/hyperv#custom-networking"), SSH_MACS("troubleshoot/ssh#no-matching-mac-found"), SSH_FEATURE_NOT_SUPPORTED("troubleshoot/ssh#requested-feature-not-supported"), SSH_JUMP_SERVERS("guide/ssh#gateways-and-jump-servers"), diff --git a/lang/strings/translations_da.properties b/lang/strings/translations_da.properties index 860291d58..4888481e8 100644 --- a/lang/strings/translations_da.properties +++ b/lang/strings/translations_da.properties @@ -1858,3 +1858,5 @@ syncBackgroundCommand=Blokering af baggrundskommando terminalBackgroundCommand=Terminal-kommando testingConnection=Test af forbindelse ... openManagementConsole=Åben administrationskonsol +keeperUseMfa=Brug 2FA-godkendelsesapp +keeperUseMfaDescription=Aktivér dette, hvis din Keeper-konto kræver en 2FA TOTP for at få adgang til adgangskoder. diff --git a/lang/strings/translations_de.properties b/lang/strings/translations_de.properties index 8f274d5e4..76b355b0b 100644 --- a/lang/strings/translations_de.properties +++ b/lang/strings/translations_de.properties @@ -1275,7 +1275,7 @@ activate=Aktivieren validUntil=Gültig bis licenseActivated=Lizenz aktiviert restart=Neustart -lockVault=Schlosstresor +lockVault=Tresor schließen restartApp=XPipe neu starten free=Kostenlos upgradeInfo=Informationen zum Upgrade auf eine Lizenz findest du weiter unten. @@ -1849,7 +1849,9 @@ commandTypeAsyncBackground=Losgelöst im Hintergrund laufen lassen commandTypeSyncBackground=Im Hintergrund laufen und auf das Ende warten commandTypeTerminalBackground=Im Terminal öffnen asyncBackgroundCommand=Hintergrund-Befehl -syncBackgroundCommand=Hintergrundbefehl blockieren +syncBackgroundCommand=Blockierender Hintergrundbefehl terminalBackgroundCommand=Terminal-Befehl testingConnection=Verbindung testen ... openManagementConsole=Verwaltungskonsole öffnen +keeperUseMfa=2FA-Authentifikator-App verwenden +keeperUseMfaDescription=Aktiviere dies, wenn dein Keeper-Konto ein 2FA TOTP erfordert, um auf Passwörter zuzugreifen. diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index db17dd65d..e8d11f44b 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1296,6 +1296,7 @@ activate=Activate validUntil=Valid until licenseActivated=License activated restart=Restart +#context: verb, to close lockVault=Lock vault restartApp=Restart XPipe #context: No payment required @@ -1882,10 +1883,13 @@ commandTypeAsyncBackground=Run detached in background commandTypeSyncBackground=Run in background and wait for finish commandTypeTerminalBackground=Open in terminal asyncBackgroundCommand=Background command +#context: A command that does not return until finished syncBackgroundCommand=Blocking background command terminalBackgroundCommand=Terminal command testingConnection=Testing connection ... openManagementConsole=Open management console +keeperUseMfa=Use 2FA authenticator app +keeperUseMfaDescription=Enable this if your Keeper account requires an 2FA TOTP to access passwords. extractReusableIdentities=Extract reusable identities identitiesAdded=Identities added syncInstantly=Sync instantly diff --git a/lang/strings/translations_es.properties b/lang/strings/translations_es.properties index 8ecf022ea..f8f5e058f 100644 --- a/lang/strings/translations_es.properties +++ b/lang/strings/translations_es.properties @@ -1817,3 +1817,5 @@ syncBackgroundCommand=Comando de fondo de bloqueo terminalBackgroundCommand=Comando de terminal testingConnection=Probar la conexión ... openManagementConsole=Consola de gestión abierta +keeperUseMfa=Utilizar la aplicación de autenticación 2FA +keeperUseMfaDescription=Actívala si tu cuenta de Keeper requiere un TOTP 2FA para acceder a las contraseñas. diff --git a/lang/strings/translations_fr.properties b/lang/strings/translations_fr.properties index 6925fed36..402002750 100644 --- a/lang/strings/translations_fr.properties +++ b/lang/strings/translations_fr.properties @@ -1857,3 +1857,5 @@ syncBackgroundCommand=Commande de blocage de l'arrière-plan terminalBackgroundCommand=Commande de terminal testingConnection=Test de connexion ... openManagementConsole=Console de gestion ouverte +keeperUseMfa=Utilise l'application 2FA authenticator +keeperUseMfaDescription=Active cette option si ton compte Keeper nécessite un TOTP 2FA pour accéder aux mots de passe. diff --git a/lang/strings/translations_id.properties b/lang/strings/translations_id.properties index 3e7a1b7a7..27f38f070 100644 --- a/lang/strings/translations_id.properties +++ b/lang/strings/translations_id.properties @@ -1817,3 +1817,5 @@ syncBackgroundCommand=Memblokir perintah latar belakang terminalBackgroundCommand=Perintah terminal testingConnection=Menguji koneksi ... openManagementConsole=Konsol manajemen terbuka +keeperUseMfa=Menggunakan aplikasi pengautentikasi 2FA +keeperUseMfaDescription=Aktifkan ini jika akun Keeper Anda memerlukan TOTP 2FA untuk mengakses kata sandi. diff --git a/lang/strings/translations_it.properties b/lang/strings/translations_it.properties index 91c58646d..c9d139c10 100644 --- a/lang/strings/translations_it.properties +++ b/lang/strings/translations_it.properties @@ -1243,7 +1243,7 @@ activate=Attivare validUntil=Valido fino a licenseActivated=Licenza attivata restart=Riavvio -lockVault=Una cassaforte con serratura +lockVault=Cassaforte con serratura restartApp=Riavviare XPipe free=Gratuito upgradeInfo=Qui di seguito puoi trovare informazioni sull'aggiornamento della licenza. @@ -1813,7 +1813,9 @@ commandTypeAsyncBackground=Eseguire in background commandTypeSyncBackground=Eseguire in background e attendere il completamento commandTypeTerminalBackground=Aprire nel terminale asyncBackgroundCommand=Comando di sfondo -syncBackgroundCommand=Comando di blocco dello sfondo +syncBackgroundCommand=Comando di blocco in background terminalBackgroundCommand=Comando del terminale testingConnection=Verifica della connessione ... openManagementConsole=Console di gestione aperta +keeperUseMfa=Usa l'applicazione autenticatore 2FA +keeperUseMfaDescription=Abilita questa opzione se il tuo account Keeper richiede un 2FA TOTP per accedere alle password. diff --git a/lang/strings/translations_ja.properties b/lang/strings/translations_ja.properties index fcead6f30..e96f7dae5 100644 --- a/lang/strings/translations_ja.properties +++ b/lang/strings/translations_ja.properties @@ -1817,3 +1817,5 @@ syncBackgroundCommand=バックグラウンドコマンドをブロックする terminalBackgroundCommand=ターミナルコマンド testingConnection=接続をテストする openManagementConsole=オープン管理コンソール +keeperUseMfa=2FA認証アプリを使用する +keeperUseMfaDescription=Keeperアカウントがパスワードにアクセスするために2FA TOTPを必要とする場合、これを有効にする。 diff --git a/lang/strings/translations_ko.properties b/lang/strings/translations_ko.properties index cd778a2d8..355f45c87 100644 --- a/lang/strings/translations_ko.properties +++ b/lang/strings/translations_ko.properties @@ -1870,3 +1870,5 @@ syncBackgroundCommand=백그라운드 명령 차단 terminalBackgroundCommand=터미널 명령 testingConnection=연결 테스트 중 ... openManagementConsole=관리 콘솔 열기 +keeperUseMfa=2FA 인증 앱 사용 +keeperUseMfaDescription=Keeper 계정에서 비밀번호에 액세스하기 위해 2FA TOTP가 필요한 경우 이 옵션을 사용 설정합니다. diff --git a/lang/strings/translations_nl.properties b/lang/strings/translations_nl.properties index 3a976b2ff..9333172f9 100644 --- a/lang/strings/translations_nl.properties +++ b/lang/strings/translations_nl.properties @@ -1813,7 +1813,9 @@ commandTypeAsyncBackground=Vrijstaand op de achtergrond uitvoeren commandTypeSyncBackground=Op de achtergrond draaien en wachten tot het klaar is commandTypeTerminalBackground=Openen in terminal asyncBackgroundCommand=Opdracht op de achtergrond -syncBackgroundCommand=Opdracht voor achtergrond blokkeren +syncBackgroundCommand=Achtergrondcommando blokkeren terminalBackgroundCommand=Terminal commando testingConnection=Verbinding testen ... openManagementConsole=Open beheerconsole +keeperUseMfa=Gebruik 2FA authenticator app +keeperUseMfaDescription=Schakel dit in als je Keeper-account een 2FA TOTP vereist om toegang te krijgen tot wachtwoorden. diff --git a/lang/strings/translations_pl.properties b/lang/strings/translations_pl.properties index 67ced1e62..1d495d404 100644 --- a/lang/strings/translations_pl.properties +++ b/lang/strings/translations_pl.properties @@ -1244,7 +1244,7 @@ activate=Aktywuj validUntil=Ważny do licenseActivated=Aktywowana licencja restart=Restart -lockVault=Skarbiec z zamkiem +lockVault=Zamknięty skarbiec restartApp=Uruchom ponownie XPipe free=Darmowy upgradeInfo=Poniżej znajdziesz informacje na temat aktualizacji do licencji. @@ -1814,7 +1814,9 @@ commandTypeAsyncBackground=Uruchom odłączony w tle commandTypeSyncBackground=Uruchom w tle i czekaj na zakończenie commandTypeTerminalBackground=Otwórz w terminalu asyncBackgroundCommand=Polecenie tła -syncBackgroundCommand=Polecenie blokowania w tle +syncBackgroundCommand=Polecenie blokujące w tle terminalBackgroundCommand=Polecenie terminala testingConnection=Testowanie połączenia ... openManagementConsole=Otwarta konsola zarządzania +keeperUseMfa=Użyj aplikacji uwierzytelniającej 2FA +keeperUseMfaDescription=Włącz tę opcję, jeśli Twoje konto Keeper wymaga TOTP 2FA, aby uzyskać dostęp do haseł. diff --git a/lang/strings/translations_pt.properties b/lang/strings/translations_pt.properties index e5f809769..a103cf555 100644 --- a/lang/strings/translations_pt.properties +++ b/lang/strings/translations_pt.properties @@ -1243,7 +1243,7 @@ activate=Ativar validUntil=Válido até licenseActivated=Licença activada restart=Reinicia -lockVault=Cofre com fechadura +lockVault=Fecha o cofre restartApp=Reinicia o XPipe free=Gratuito upgradeInfo=Podes encontrar informações sobre a atualização para uma licença abaixo. @@ -1813,7 +1813,9 @@ commandTypeAsyncBackground=Executa a desanexação em segundo plano commandTypeSyncBackground=Corre em segundo plano e espera pela conclusão commandTypeTerminalBackground=Abre no terminal asyncBackgroundCommand=Comando de fundo -syncBackgroundCommand=Bloqueio do comando de fundo +syncBackgroundCommand=Bloqueia o comando de fundo terminalBackgroundCommand=Comando de terminal testingConnection=Testar a ligação ... openManagementConsole=Abre a consola de gestão +keeperUseMfa=Utiliza a aplicação de autenticação 2FA +keeperUseMfaDescription=Ative essa opção se a sua conta do Keeper exigir um TOTP 2FA para acessar senhas. diff --git a/lang/strings/translations_ru.properties b/lang/strings/translations_ru.properties index 6e4692a88..87f24d0ad 100644 --- a/lang/strings/translations_ru.properties +++ b/lang/strings/translations_ru.properties @@ -1925,7 +1925,9 @@ commandTypeAsyncBackground=Запускать detached в фоновом реж commandTypeSyncBackground=Запустить в фоновом режиме и дождаться завершения commandTypeTerminalBackground=Открыть в терминале asyncBackgroundCommand=Фоновая команда -syncBackgroundCommand=Блокировка фоновой команды +syncBackgroundCommand=Блокирующая фоновая команда terminalBackgroundCommand=Команда терминала testingConnection=Тестирование соединения ... openManagementConsole=Открытая консоль управления +keeperUseMfa=Используйте приложение аутентификатора 2FA +keeperUseMfaDescription=Включи эту опцию, если твой аккаунт Keeper требует 2FA TOTP для доступа к паролям. diff --git a/lang/strings/translations_sv.properties b/lang/strings/translations_sv.properties index 52c7c3387..86b7e193b 100644 --- a/lang/strings/translations_sv.properties +++ b/lang/strings/translations_sv.properties @@ -1817,3 +1817,5 @@ syncBackgroundCommand=Blockering av bakgrundskommando terminalBackgroundCommand=Kommando för terminal testingConnection=Testning av anslutning ... openManagementConsole=Öppen hanteringskonsol +keeperUseMfa=Använd 2FA-autentiseringsapp +keeperUseMfaDescription=Aktivera detta om ditt Keeper-konto kräver en 2FA TOTP för att komma åt lösenord. diff --git a/lang/strings/translations_tr.properties b/lang/strings/translations_tr.properties index edc756804..41c50c9b7 100644 --- a/lang/strings/translations_tr.properties +++ b/lang/strings/translations_tr.properties @@ -1817,3 +1817,5 @@ syncBackgroundCommand=Arka plan komutunu engelleme terminalBackgroundCommand=Terminal komutu testingConnection=Test bağlantısı ... openManagementConsole=Açık yönetim konsolu +keeperUseMfa=2FA kimlik doğrulayıcı uygulamasını kullanın +keeperUseMfaDescription=Keeper hesabınız parolalara erişmek için bir 2FA TOTP gerektiriyorsa bunu etkinleştirin. diff --git a/lang/strings/translations_vi.properties b/lang/strings/translations_vi.properties index a053d6678..d3638ed10 100644 --- a/lang/strings/translations_vi.properties +++ b/lang/strings/translations_vi.properties @@ -1243,7 +1243,7 @@ activate=Kích hoạt validUntil=Hiệu lực đến licenseActivated=Giấy phép đã được kích hoạt restart=Khởi động lại -lockVault=Kho lưu trữ an toàn +lockVault=Khoá két sắt restartApp=Khởi động lại XPipe free=Miễn phí upgradeInfo=Cậu có thể tìm thấy thông tin về việc nâng cấp lên giấy phép ở phần dưới đây. @@ -1813,7 +1813,9 @@ commandTypeAsyncBackground=Chạy độc lập trong nền commandTypeSyncBackground=Chạy ngầm và chờ hoàn tất commandTypeTerminalBackground=Mở trong terminal asyncBackgroundCommand=Lệnh nền -syncBackgroundCommand=Chặn lệnh nền +syncBackgroundCommand=Lệnh nền chặn terminalBackgroundCommand=Lệnh terminal testingConnection=Kiểm tra kết nối ... openManagementConsole=Mở bảng điều khiển quản lý +keeperUseMfa=Sử dụng ứng dụng xác thực hai yếu tố (2FA) +keeperUseMfaDescription=Bật tùy chọn này nếu tài khoản Keeper của cậu yêu cầu xác thực hai yếu tố (2FA) bằng mã TOTP để truy cập mật khẩu. diff --git a/lang/strings/translations_zh-Hans.properties b/lang/strings/translations_zh-Hans.properties index 70157ab36..d4ee596a2 100644 --- a/lang/strings/translations_zh-Hans.properties +++ b/lang/strings/translations_zh-Hans.properties @@ -2444,3 +2444,5 @@ syncBackgroundCommand=阻止后台命令 terminalBackgroundCommand=终端命令 testingConnection=测试连接... openManagementConsole=开放式管理控制台 +keeperUseMfa=使用 2FA 验证器应用程序 +keeperUseMfaDescription=如果 Keeper 帐户要求使用 2FA TOTP 访问密码,请启用此选项。 diff --git a/lang/strings/translations_zh-Hant.properties b/lang/strings/translations_zh-Hant.properties index d36d5c5cd..3dad4a179 100644 --- a/lang/strings/translations_zh-Hant.properties +++ b/lang/strings/translations_zh-Hant.properties @@ -1243,7 +1243,7 @@ activate=啟動 validUntil=有效期至 licenseActivated=已啟用的授權 restart=重新啟動 -lockVault=鎖庫 +lockVault=鎖金庫 restartApp=重新啟動 XPipe free=免費 upgradeInfo=您可以在下方找到升級為授權的相關資訊。 @@ -1817,3 +1817,5 @@ syncBackgroundCommand=封鎖背景指令 terminalBackgroundCommand=終端命令 testingConnection=測試連接 ... openManagementConsole=開放式管理主控台 +keeperUseMfa=使用 2FA 認證器應用程式 +keeperUseMfaDescription=如果您的 Keeper 帳戶需要 2FA TOTP 才能存取密碼,請啟用此項。