mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-06-04 21:58:02 -04:00
Rework
This commit is contained in:
@@ -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<DataStoreEntry> selected;
|
||||
private final Predicate<StoreEntryWrapper> applicable;
|
||||
private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
|
||||
@@ -58,6 +60,13 @@ public final class BrowserConnectionListComp extends SimpleRegionBuilder {
|
||||
&& newValue.equals(s.getWrapper().getEntry()));
|
||||
});
|
||||
});
|
||||
busyEntries.addListener((SetChangeListener<? super StoreSection>) change -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.pseudoClassStateChanged(
|
||||
BUSY,
|
||||
change.getSet().contains(s));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
183
app/src/main/java/io/xpipe/app/core/AppCertStore.java
Normal file
183
app/src/main/java/io/xpipe/app/core/AppCertStore.java
Normal file
@@ -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<X509Certificate> certificates;
|
||||
private X509TrustManager trustManager;
|
||||
private final SavingTrustManager savingTrustManager = new SavingTrustManager();
|
||||
|
||||
private AppCertStore(List<X509Certificate> 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<X509Certificate>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -95,6 +95,10 @@ public class ShellSession extends Session {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (secs < 30) {
|
||||
secs = 30;
|
||||
}
|
||||
|
||||
return shellControl.isInactive(Duration.ofSeconds(secs));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Throwable>();
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ public final class AppPrefs {
|
||||
.valueClass(ShellScript.class)
|
||||
.log(false)
|
||||
.build());
|
||||
final Property<HttpProxy> httpProxy = map(Mapping.builder()
|
||||
final ObjectProperty<HttpProxy> 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 =
|
||||
|
||||
@@ -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<Boolean>();
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<DataStore> 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;
|
||||
}
|
||||
|
||||
217
app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java
Normal file
217
app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java
Normal file
@@ -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<List<?>> sans = cert.getSubjectAlternativeNames();
|
||||
if (sans != null && !sans.isEmpty()) {
|
||||
List<String> dns = new ArrayList<>();
|
||||
List<String> ips = new ArrayList<>();
|
||||
List<String> 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<String, String> 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<String, String> parseDn(String dn) {
|
||||
Map<String, String> 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
2
dist/changelog/23.0.md
vendored
2
dist/changelog/23.0.md
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
2
lang/strings/fixed_en.properties
generated
2
lang/strings/fixed_en.properties
generated
@@ -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
|
||||
|
||||
18
lang/strings/translations_en.properties
generated
18
lang/strings/translations_en.properties
generated
@@ -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.
|
||||
#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
|
||||
Reference in New Issue
Block a user