From 680ce2f4bf74ef8778a2d8a78fff5ef84059ae09 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 10 May 2026 17:59:35 +0000 Subject: [PATCH] Rework --- .../file/BrowserConnectionListComp.java | 9 + .../java/io/xpipe/app/core/AppCertStore.java | 183 +++++++++++++++ .../io/xpipe/app/core/mode/AppBaseMode.java | 3 + .../java/io/xpipe/app/ext/ShellSession.java | 4 + .../java/io/xpipe/app/issue/ErrorEvent.java | 23 +- .../io/xpipe/app/issue/ErrorEventFactory.java | 26 ++- .../io/xpipe/app/issue/GuiErrorHandler.java | 2 +- .../java/io/xpipe/app/prefs/AppPrefs.java | 4 +- .../io/xpipe/app/prefs/HttpProxyCategory.java | 16 +- .../app/pwman/PassboltPasswordManager.java | 3 +- .../xpipe/app/terminal/WarpTerminalType.java | 4 +- .../io/xpipe/app/util/DocumentationLink.java | 1 - .../java/io/xpipe/app/util/HttpHelper.java | 16 +- .../java/io/xpipe/app/util/HttpProxy.java | 14 ++ .../xpipe/app/util/TlsCertificateFormat.java | 217 ++++++++++++++++++ .../io/xpipe/app/resources/style/bookmark.css | 4 + dist/changelog/23.0.md | 2 + .../base/identity/IdentityChoiceBuilder.java | 16 +- lang/strings/fixed_en.properties | 2 +- lang/strings/translations_en.properties | 18 +- 20 files changed, 528 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/core/AppCertStore.java create mode 100644 app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java index 7294b8c27..503fb447b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java @@ -9,6 +9,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.SetChangeListener; import javafx.css.PseudoClass; import javafx.scene.control.Button; import javafx.scene.layout.Region; @@ -21,6 +22,7 @@ import java.util.function.Predicate; public final class BrowserConnectionListComp extends SimpleRegionBuilder { private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + private static final PseudoClass BUSY = PseudoClass.getPseudoClass("busy"); private final ObservableValue selected; private final Predicate applicable; private final BiConsumer action; @@ -58,6 +60,13 @@ public final class BrowserConnectionListComp extends SimpleRegionBuilder { && newValue.equals(s.getWrapper().getEntry())); }); }); + busyEntries.addListener((SetChangeListener) change -> { + PlatformThread.runLaterIfNeeded(() -> { + struc.pseudoClassStateChanged( + BUSY, + change.getSet().contains(s)); + }); + }); }); }; diff --git a/app/src/main/java/io/xpipe/app/core/AppCertStore.java b/app/src/main/java/io/xpipe/app/core/AppCertStore.java new file mode 100644 index 000000000..14002eac9 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/AppCertStore.java @@ -0,0 +1,183 @@ +package io.xpipe.app.core; + +import io.xpipe.app.comp.base.ModalButton; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.comp.base.TextAreaComp; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.platform.OptionsBuilder; +import io.xpipe.app.process.OsFileSystem; +import io.xpipe.app.util.DocumentationLink; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.app.util.TlsCertificateFormat; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import lombok.SneakyThrows; +import org.apache.commons.io.FilenameUtils; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.*; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Enumeration; +import java.util.List; + +public class AppCertStore { + + private class SavingTrustManager implements X509TrustManager { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManager.getAcceptedIssuers(); + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + trustManager.checkClientTrusted(chain, authType); + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + try { + trustManager.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + var cause = e.getCause(); + var nonTrusted = cause != null && cause.getClass().getName().equals("sun.security.provider.certpath.SunCertPathBuilderException"); + if (nonTrusted) { + showTrustDialog(chain[chain.length - 1]); + ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(e) + .expected() + .omit()); + throw e; + } else { + throw ErrorEventFactory.expected(e); + } + } + } + } + + private final List certificates; + private X509TrustManager trustManager; + private final SavingTrustManager savingTrustManager = new SavingTrustManager(); + + private AppCertStore(List certificates) {this.certificates = certificates;} + + public void addCertificate(String name, X509Certificate certificate) { + try { + var dir = AppProperties.get().getDataDir().resolve("cacerts"); + Files.createDirectories(dir); + var file = dir.resolve(OsFileSystem.ofLocal().makeFileSystemCompatible(name) + ".pem"); + var s = convertToPem(certificate); + Files.writeString(file, s); + certificates.add(certificate); + updateTrustManager(); + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); + } + } + + public X509TrustManager getCustomTrustManager() { + return savingTrustManager; + } + + @SneakyThrows + private void updateTrustManager() { + KeyStore ks = KeyStore.getInstance("JKS"); + + var caCertsFile = Path.of(System.getProperty("java.home") + "/lib/security/cacerts"); + try (FileInputStream fis = new FileInputStream(caCertsFile.toFile())) { + ks.load(fis, null); + } + + for (int i = 0; i < certificates.size(); i++) { + ks.setCertificateEntry(i + "", certificates.get(i)); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + trustManager = (X509TrustManager) tmf.getTrustManagers()[0]; + } + + private void showTrustDialog(X509Certificate certificate) { + var format = TlsCertificateFormat.format(certificate); + var content = new TextAreaComp(new SimpleStringProperty(format)) + .applyStructure(structure -> { + structure.getTextArea().setEditable(false); + }) + .prefHeight(450); + var name = new SimpleStringProperty(); + var options = new OptionsBuilder() + .nameAndDescription("certificateDetails") + .addComp(content) + .nameAndDescription("certificateName") + .addString(name) + .nonNull() + .buildComp() + .prefWidth(650); + var modal = ModalOverlay.of("untrustedCertificateTitle", options); + modal.addButton(ModalButton.cancel()); + modal.addButton(new ModalButton("trust", () -> { + ThreadHelper.runAsync(() -> { + addCertificate(name.getValue(), certificate); + }); + }, true, true).augment(button -> { + button.disableProperty().bind(name.isNull()); + })); + modal.show(); + } + + private static AppCertStore INSTANCE; + + public static AppCertStore get() { + return INSTANCE; + } + + public static void init() { + var dir = AppProperties.get().getDataDir().resolve("cacerts"); + if (!Files.exists(dir)) { + INSTANCE = new AppCertStore(new ArrayList<>()); + INSTANCE.updateTrustManager(); + return; + } + + var list = new ArrayList(); + try (var stream = Files.list(dir)) { + var files = stream.toList(); + for (Path f : files) { + var cert = parseCertificate(f); + list.add(cert); + } + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).expected().handle(); + } + + INSTANCE = new AppCertStore(list); + INSTANCE.updateTrustManager(); + } + + public static void reset() { + INSTANCE = null; + } + + private static X509Certificate parseCertificate(Path file) throws Exception { + var b = Files.readAllBytes(file); + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(b)); + } + + private static String convertToPem(X509Certificate cert) throws CertificateEncodingException { + String begin = "-----BEGIN CERTIFICATE-----\n"; + String end = "\n-----END CERTIFICATE-----\n"; + byte[] derCert = cert.getEncoded(); + Base64.Encoder encoder = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)); + String pemCertPre = encoder.encodeToString(derCert); + String pemCert = begin + pemCertPre + end; + return pemCert; + } +} diff --git a/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java index ecaa68545..a09564e68 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/AppBaseMode.java @@ -28,6 +28,7 @@ import io.xpipe.app.prefs.WorkspaceManager; import io.xpipe.app.process.LocalShell; import io.xpipe.app.pwman.KeePassXcPasswordManager; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.core.AppCertStore; import io.xpipe.app.storage.DataStorageSyncHandler; import io.xpipe.app.terminal.TerminalDockHubManager; import io.xpipe.app.terminal.TerminalLauncherManager; @@ -64,6 +65,7 @@ public class AppBaseMode extends AppOperationMode { // if (true) throw new IllegalStateException(); TrackEvent.info("Initializing base mode components ..."); + AppCertStore.init(); AppMainWindow.loadingText("checkingLicense"); LicenseProvider.get().init(); AppMainWindow.loadingText("initializingApp"); @@ -229,6 +231,7 @@ public class AppBaseMode extends AppOperationMode { AppDataLock.unlock(); BlobManager.reset(); FileBridge.reset(); + AppCertStore.reset(); AppFileWatcher.reset(); GlobalTimer.reset(); LocalFileTracker.reset(); diff --git a/app/src/main/java/io/xpipe/app/ext/ShellSession.java b/app/src/main/java/io/xpipe/app/ext/ShellSession.java index 85c26ac77..33196845f 100644 --- a/app/src/main/java/io/xpipe/app/ext/ShellSession.java +++ b/app/src/main/java/io/xpipe/app/ext/ShellSession.java @@ -95,6 +95,10 @@ public class ShellSession extends Session { return false; } + if (secs < 30) { + secs = 30; + } + return shellControl.isInactive(Duration.ofSeconds(secs)); } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java index 7f6450250..4ed64eecd 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java @@ -85,6 +85,25 @@ public class ErrorEvent { public static class ErrorEventBuilder { + public void apply(ErrorEventBuilder other) { + var e = other.build(); + if (!e.reportable) { + expected(); + } + if (e.terminal) { + term(); + } + if (e.link != null) { + link = e.link; + } + if (e.description != null) { + description = e.description; + } + if (e.omitted) { + omit(); + } + } + public ErrorEventBuilder documentationLink(DocumentationLink documentationLink) { return link(documentationLink.getLink()); } @@ -133,9 +152,5 @@ public class ErrorEvent { Throwable getThrowable() { return throwable; } - - String getLink() { - return link; - } } } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorEventFactory.java b/app/src/main/java/io/xpipe/app/issue/ErrorEventFactory.java index ead8bd6f0..bea22b840 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEventFactory.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEventFactory.java @@ -7,10 +7,7 @@ import io.xpipe.core.OsType; import java.nio.file.AccessDeniedException; import java.nio.file.NoSuchFileException; -import java.util.Arrays; -import java.util.IdentityHashMap; -import java.util.Locale; -import java.util.Map; +import java.util.*; import javax.net.ssl.SSLHandshakeException; public class ErrorEventFactory { @@ -53,6 +50,12 @@ public class ErrorEventFactory { } public static synchronized void preconfigure(ErrorEvent.ErrorEventBuilder event) { + var found = EVENT_BASES.get(event.getThrowable()); + if (found != null) { + found.apply(event); + return; + } + EVENT_BASES.put(event.getThrowable(), event); } @@ -62,12 +65,17 @@ public class ErrorEventFactory { b = ErrorEvent.builder().throwable(t); } - if (t instanceof SSLHandshakeException - || (t.getClass().getName().equals("sun.security.provider.certpath.SunCertPathBuilderException"))) { - if (b.getLink() == null) { - b.documentationLink(DocumentationLink.TLS_DECRYPTION); + var chain = new ArrayList(); + var current = t; + while (current != null) { + chain.addFirst(current); + current = current.getCause(); + } + for (Throwable pre : chain) { + var found = EVENT_BASES.get(pre); + if (found != null) { + b.apply(found); } - b.expected(); } // Indicates that the session is scheduled to end and new processes won't be started diff --git a/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java index 4b9282584..9bdce464b 100644 --- a/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java @@ -20,7 +20,7 @@ public class GuiErrorHandler extends GuiErrorHandlerBase implements ErrorHandler public void handle(ErrorEvent event) { log.handle(event); - if (event.isOmitted() && PlatformState.getCurrent() == PlatformState.RUNNING) { + if (event.isOmitted() && PlatformState.getCurrent() != PlatformState.RUNNING) { return; } diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index 3a9e2c1fa..faac604b0 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -283,7 +283,7 @@ public final class AppPrefs { .valueClass(ShellScript.class) .log(false) .build()); - final Property httpProxy = map(Mapping.builder() + final ObjectProperty httpProxy = map(Mapping.builder() .property(new GlobalObjectProperty<>()) .key("httpProxy") .valueClass(HttpProxy.class) @@ -377,7 +377,7 @@ public final class AppPrefs { final BooleanProperty checkForSecurityUpdates = mapLocal(new GlobalBooleanProperty(true), "checkForSecurityUpdates", Boolean.class, false); final BooleanProperty disableHttpsTlsCheck = - mapLocal(new GlobalBooleanProperty(false), "disableHttpsTlsCheck", Boolean.class, true); + mapLocal(new GlobalBooleanProperty(false), "disableHttpsTlsCheck", Boolean.class, false); final BooleanProperty condenseConnectionDisplay = mapLocal(new GlobalBooleanProperty(false), "condenseConnectionDisplay", Boolean.class, false); final BooleanProperty showChildCategoriesInParentCategory = diff --git a/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java b/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java index a851353b9..a3638015c 100644 --- a/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java @@ -15,6 +15,7 @@ import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.HttpProxy; +import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; public class HttpProxyCategory extends AppPrefsCategory { @@ -32,10 +33,23 @@ public class HttpProxyCategory extends AppPrefsCategory { @Override protected BaseRegionBuilder create() { var prefs = AppPrefs.get(); + var disableTlsReadOnly = Bindings.createBooleanBinding(() -> { + return prefs.httpProxy.get() != null && prefs.httpProxy.get().isDisableTlsVerification(); + }, prefs.httpProxy); + var disableTlsReadOnlyProp = new SimpleObjectProperty(); + disableTlsReadOnlyProp.bind(disableTlsReadOnly); return new OptionsBuilder() .title("httpProxyConfiguration") .sub(proxy()) - .sub(new OptionsBuilder().pref(prefs.disableHttpsTlsCheck).addToggle(prefs.disableHttpsTlsCheck)) + .sub(new OptionsBuilder() + .pref(prefs.disableHttpsTlsCheck) + .addToggle(prefs.disableHttpsTlsCheck) + .hide(prefs.httpProxy.isNotNull()) + .nameAndDescription("disableHttpsTlsCheck") + .addToggle(disableTlsReadOnlyProp) + .disable() + .hide(prefs.httpProxy.isNull()) + ) .buildComp(); } diff --git a/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java index 5710534f6..8ece5fa3b 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PassboltPasswordManager.java @@ -15,6 +15,7 @@ import io.xpipe.app.process.CommandSupport; import io.xpipe.app.process.LocalShell; import io.xpipe.app.process.ShellControl; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.HttpProxy; import io.xpipe.core.FilePath; import io.xpipe.core.InPlaceSecretValue; import io.xpipe.core.JacksonMapper; @@ -181,7 +182,7 @@ public class PassboltPasswordManager implements PasswordManager { return null; } - b.addIf(AppPrefs.get().disableHttpsTlsCheck().getValue(), "--tlsSkipVerify") + b.addIf(HttpProxy.disableTlsVerification(), "--tlsSkipVerify") .add("--serverAddress") .addLiteral(serverUrl) .add("--userPassword") diff --git a/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java index 546ac6e3c..9f21b1527 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java @@ -39,9 +39,7 @@ public interface WarpTerminalType extends ExternalTerminalType, TrackableTermina @Override default boolean isRecommended() { - // Right now, opening scripts is broken - // Maybe this will be fixed at some point by Warp - return false; + return true; } @Override 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 30457d60c..e465e68df 100644 --- a/app/src/main/java/io/xpipe/app/util/DocumentationLink.java +++ b/app/src/main/java/io/xpipe/app/util/DocumentationLink.java @@ -11,7 +11,6 @@ public enum DocumentationLink { MACOS_SETUP("guide/installation#macos"), DOUBLE_PROMPT("troubleshoot/two-step-connections"), LICENSE_ACTIVATION("troubleshoot/license-activation"), - TLS_DECRYPTION("troubleshoot/license-activation#tls-decryption"), UPDATE_FAIL("troubleshoot/update-fail"), PRIVACY("legal/privacy-policy"), EULA("legal/end-user-license-agreement"), diff --git a/app/src/main/java/io/xpipe/app/util/HttpHelper.java b/app/src/main/java/io/xpipe/app/util/HttpHelper.java index 41e9fc2bc..f802d9d0c 100644 --- a/app/src/main/java/io/xpipe/app/util/HttpHelper.java +++ b/app/src/main/java/io/xpipe/app/util/HttpHelper.java @@ -6,9 +6,10 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.core.AppCertStore; import lombok.SneakyThrows; -import java.io.IOException; +import java.io.*; import java.net.*; import java.net.http.HttpClient; import java.net.http.HttpResponse; @@ -17,9 +18,7 @@ import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.List; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; +import javax.net.ssl.*; public class HttpHelper { @@ -28,7 +27,7 @@ public class HttpHelper { var proxy = HttpProxy.getActiveProxy(); return client( proxy.orElse(null), - AppPrefs.get() != null && AppPrefs.get().disableHttpsTlsCheck().getValue()); + HttpProxy.disableTlsVerification()); } @SneakyThrows @@ -54,6 +53,13 @@ public class HttpHelper { }; sslContext.init(null, new TrustManager[] {trustManager}, new SecureRandom()); builder.sslContext(sslContext); + } else { + var certStore = AppCertStore.get(); + if (certStore != null) { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[]{certStore.getCustomTrustManager()}, null); + builder.sslContext(context); + } } if (proxy != null) { diff --git a/app/src/main/java/io/xpipe/app/util/HttpProxy.java b/app/src/main/java/io/xpipe/app/util/HttpProxy.java index 94d8a55e0..0cd66abc3 100644 --- a/app/src/main/java/io/xpipe/app/util/HttpProxy.java +++ b/app/src/main/java/io/xpipe/app/util/HttpProxy.java @@ -33,6 +33,15 @@ public class HttpProxy { return Optional.of(current); } + public static boolean disableTlsVerification() { + var a = getActiveProxy(); + if (a.isPresent()) { + return a.get().isDisableTlsVerification(); + } else { + return AppPrefs.get() != null && AppPrefs.get().disableHttpsTlsCheck().getValue(); + } + } + public static boolean canUseAsProxy(DataStoreEntryRef ref) { if (!ref.get().getValidity().isUsable()) { return false; @@ -52,9 +61,14 @@ public class HttpProxy { + port; } + public boolean hasAuth() { + return user != null && password != null; + } + String host; int port; String user; InPlaceSecretValue password; boolean socks5; + boolean disableTlsVerification; } diff --git a/app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java b/app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java new file mode 100644 index 000000000..85ffe000a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java @@ -0,0 +1,217 @@ +package io.xpipe.app.util; + +import java.security.MessageDigest; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAKey; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.*; + +public class TlsCertificateFormat { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC); + + public static String format(X509Certificate cert) { + var sb = new StringBuilder(); + + boolean selfSigned = cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()); + + sb.append(" Issued To\n"); + appendDnFields(sb, cert.getSubjectX500Principal().getName()); + + sb.append("\n Issued By"); + if (selfSigned) { + sb.append(" [!] SELF-SIGNED — not trusted by any CA"); + } + sb.append("\n"); + if (!selfSigned) { + appendDnFields(sb, cert.getIssuerX500Principal().getName()); + } + + sb.append("\n Validity\n"); + Instant now = Instant.now(); + Instant notBefore = cert.getNotBefore().toInstant(); + Instant notAfter = cert.getNotAfter().toInstant(); + long daysLeft = ChronoUnit.DAYS.between(now, notAfter); + + sb.append(" Issued On : ").append(DATE_FMT.format(notBefore)).append("\n"); + sb.append(" Expires On : ").append(DATE_FMT.format(notAfter)); + + if (now.isBefore(notBefore)) { + sb.append(" [!] NOT YET VALID"); + } else if (daysLeft < 0) { + sb.append(" [!] EXPIRED ").append(Math.abs(daysLeft)).append(" days ago"); + } else if (daysLeft <= 30) { + sb.append(" [!] expires in ").append(daysLeft).append(" days"); + } else { + sb.append(" (").append(daysLeft).append(" days remaining)"); + } + sb.append("\n"); + + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans != null && !sans.isEmpty()) { + List dns = new ArrayList<>(); + List ips = new ArrayList<>(); + List other = new ArrayList<>(); + for (List san : sans) { + int type = (Integer) san.get(0); + String val = String.valueOf(san.get(1)); + switch (type) { + case 2 -> dns.add(val); + case 7 -> ips.add(val); + default -> other.add(sanTypeName(type) + ":" + val); + } + } + sb.append("\n Valid For Hostnames\n"); + dns.forEach(h -> sb.append(" ").append(h).append("\n")); + ips.forEach(ip -> sb.append(" IP: ").append(ip).append("\n")); + other.forEach(o -> sb.append(" ").append(o).append("\n")); + } + } catch (Exception ignored) { + } + + sb.append("\n Fingerprints\n"); + try { + byte[] encoded = cert.getEncoded(); + sb.append(" SHA-256 : ").append(fingerprint(encoded, "SHA-256")).append("\n"); + sb.append(" SHA-1 : ").append(fingerprint(encoded, "SHA-1")).append("\n"); + } catch (Exception ignored) { + } + + return sb.toString(); + } + + private static void appendDnFields(StringBuilder sb, String dn) { + Map fields = parseDn(dn); + append(sb, "Common Name (CN)", fields.get("CN")); + append(sb, "Organization (O)", fields.get("O")); + append(sb, "Unit (OU) ", fields.get("OU")); + append(sb, "Locality (L) ", fields.get("L")); + append(sb, "State (ST) ", fields.get("ST")); + append(sb, "Country (C) ", fields.get("C")); + } + + private static void append(StringBuilder sb, String label, String value) { + if (value != null) { + sb.append(" ").append(label).append(" : ").append(value).append("\n"); + } + } + + private static Map parseDn(String dn) { + Map map = new LinkedHashMap<>(); + int i = 0; + int len = dn.length(); + + while (i < len) { + while (i < len && dn.charAt(i) == ' ') + i++; + if (i >= len) { + break; + } + + int typeStart = i; + while (i < len && dn.charAt(i) != '=') + i++; + if (i >= len) { + break; + } + String type = dn.substring(typeStart, i).trim().toUpperCase(); + i++; + + while (i < len && dn.charAt(i) == ' ') + i++; + + var value = new StringBuilder(); + + if (i < len && dn.charAt(i) == '#') { + i++; + while (i < len && isHex(dn.charAt(i))) { + value.append(dn.charAt(i)); + i++; + } + map.putIfAbsent(type, "#" + value); + while (i < len && dn.charAt(i) != ',' && dn.charAt(i) != ';') + i++; + if (i < len) { + i++; + } + continue; + } + + boolean quoted = i < len && dn.charAt(i) == '"'; + if (quoted) { + i++; + } + + outer: + while (i < len) { + char c = dn.charAt(i); + if (quoted) { + if (c == '"') { + i++; + break; + } + } else { + switch (c) { + case ',': + case ';': + i++; + break outer; + case '+': + i++; + break outer; + } + } + if (c == '\\' && i + 1 < len) { + i++; + char next = dn.charAt(i); + if (isHex(next) && i + 1 < len && isHex(dn.charAt(i + 1))) { + value.append((char) Integer.parseInt(dn.substring(i, i + 2), 16)); + i += 2; + } else { + value.append(next); + i++; + } + continue; + } + value.append(c); + i++; + } + + map.putIfAbsent(type, value.toString().trim()); + } + return map; + } + + private static boolean isHex(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + private static String fingerprint(byte[] encoded, String algorithm) throws Exception { + byte[] digest = MessageDigest.getInstance(algorithm).digest(encoded); + StringBuilder hex = new StringBuilder(); + for (int i = 0; i < digest.length; i++) { + if (i > 0) { + hex.append(':'); + } + hex.append(String.format("%02X", digest[i])); + } + return hex.toString(); + } + + private static String sanTypeName(int type) { + return switch (type) { + case 0 -> "otherName"; + case 1 -> "email"; + case 3 -> "x400Address"; + case 4 -> "directoryName"; + case 5 -> "ediPartyName"; + case 6 -> "URI"; + case 8 -> "registeredID"; + default -> "type" + type; + }; + } +} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css index 6ee50148a..b315b2add 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css @@ -17,6 +17,10 @@ -fx-font-weight: BOLD; } +.bookmark-list .store-section-mini-comp .item:busy { + -fx-opacity: 0.5; +} + .bookmark-list .store-section-mini-comp:top { -fx-padding: 0 0 0 2px; } diff --git a/dist/changelog/23.0.md b/dist/changelog/23.0.md index 733a7b77c..0d5a74bad 100644 --- a/dist/changelog/23.0.md +++ b/dist/changelog/23.0.md @@ -47,6 +47,8 @@ If you are running XPipe in an enterprise environment behind a proxy, you can al - Make explicit display scale value only accept multiples of 25% to prevent display issues - Add synchronization when multiple FIDO2 SSH connections are started to prevent failures caused by concurrent security key requests - Switch vault key generation to argon2 for improved post quantum security of the vault +- Add option to automatically exit background shell sessions after an inactivity period +- Add move and delete actions for batch selections ## Fixes diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java index e5b3112a9..9b3868df4 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java @@ -246,15 +246,15 @@ public class IdentityChoiceBuilder { : null; if (u == null && p == null && i == null) { return null; - } else { - return IdentityValue.InPlace.builder() - .identityStore(LocalIdentityStore.builder() - .username(u) - .password(p) - .sshIdentity(i) - .build()) - .build(); } + + return IdentityValue.InPlace.builder() + .identityStore(LocalIdentityStore.builder() + .username(u) + .password(p) + .sshIdentity(i) + .build()) + .build(); } }, identity); diff --git a/lang/strings/fixed_en.properties b/lang/strings/fixed_en.properties index a6a5efc57..f304308cf 100644 --- a/lang/strings/fixed_en.properties +++ b/lang/strings/fixed_en.properties @@ -154,7 +154,7 @@ nixDist=Nix antigravity=Antigravity rsa=RSA ed25519=ED25519 -ed25519Sk=ED25519 (FIDO2) +ed25519Sk=ED25519-SK (FIDO2) westonEditor=Weston Editor passbolt=Passbolt yakuake=Yakuake diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index ab51ef93f..1b353ed97 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -2107,8 +2107,10 @@ openSettingsDescription=Navigate to the corresponding settings entry toggleSizeLock=Toggle size lock useExternalNetcatForProxies=Use externally installed netcat executable for proxies useExternalNetcatForProxiesDescription=Use locally installed netcat executable to proxy SSH connections instead of the built-in proxy functionality if possible. This requires ncat to be installed on Windows and nc on other operating systems. -networkProxy.displayName=Network proxy -networkProxy.displayDescription=Configure a proxy server to use as a gateway for connections +#force +networkProxy.displayName=HTTP/SOCKS proxy +#force +networkProxy.displayDescription=Configure an HTTP/SOCKS proxy server to use as a gateway networkProxyType=Proxy type networkProxyTypeDescription=The protocol of the proxy server networkProxyHost=Host @@ -2173,4 +2175,14 @@ availableShells=Available shells #context: verb keepEnabled=Keep enabled backgroundSessionInactivityTimeout=Background shell inactivity timeout -backgroundSessionInactivityTimeoutDescription=The amount of seconds to close background shell sessions if they were not used.\n\nBy default, XPipe will keep shell connections to remote systems open in the background to reuse them later on and speed up operations. If you want to automatically close those in a timely manner, you can set this option. \ No newline at end of file +#force +backgroundSessionInactivityTimeoutDescription=The amount of seconds to close background shell sessions after, if they were not used.\n\nBy default, XPipe will keep shell connections to remote systems open in the background to reuse them later on and speed up operations. If you want to automatically close those in a timely manner, you can set this option. +disableTlsVerification=Disable TLS verification +disableTlsVerificationDescription=Don't verify the TLS certificate, in case HTTPS traffic is intercepted by the proxy +certificateDetails=Certificate details +certificateDetailsDescription=Check whether this is the correct certificate +certificateName=Name +certificateNameDescription=Give the stored certificate a recognizable name +#context: verb +trust=Trust +untrustedCertificateTitle=Untrusted certificate \ No newline at end of file