diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java index 3cd48b898..a21f5b2a3 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java @@ -10,6 +10,7 @@ import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.util.BooleanScope; import io.xpipe.core.OsType; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; @@ -70,6 +71,11 @@ public class ModalOverlayComp extends RegionBuilder { } }); } + + @Override + protected Timeline createCloseBlockedAnimation() { + return new Timeline(); + } }); modal.setInTransitionFactory( OsType.ofLocal() == OsType.LINUX ? null : node -> Animations.fadeIn(node, Duration.millis(150))); diff --git a/app/src/main/java/io/xpipe/app/cred/InPlaceKeyStrategy.java b/app/src/main/java/io/xpipe/app/cred/InPlaceKeyStrategy.java index 0c69b9f52..68911b3f2 100644 --- a/app/src/main/java/io/xpipe/app/cred/InPlaceKeyStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/InPlaceKeyStrategy.java @@ -104,8 +104,7 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy { }), key) .nonNull() - .name("keyPassword") - .description("sshConfigHost.identityPassphraseDescription") + .nameAndDescription("keyPassphrase") .sub(passwordChoice, keyPasswordProperty) .nonNull() .nameAndDescription("inPlacePublicKey") diff --git a/app/src/main/java/io/xpipe/app/cred/KeyFileStrategy.java b/app/src/main/java/io/xpipe/app/cred/KeyFileStrategy.java index 54c180bc3..6a0d76847 100644 --- a/app/src/main/java/io/xpipe/app/cred/KeyFileStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/KeyFileStrategy.java @@ -167,8 +167,7 @@ public class KeyFileStrategy implements SshIdentityStrategy { false), keyPath) .nonNull() - .name("keyPassword") - .description("sshConfigHost.identityPassphraseDescription") + .nameAndDescription("keyPassphrase") .sub(passwordChoice, keyPasswordProperty) .nonNull() .nameAndDescription("inPlacePublicKey") diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java b/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java index f48e16d88..5010dcee6 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java @@ -21,7 +21,8 @@ public enum DataStoreCreationCategory { SERIAL(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID), MACRO(DataStorage.ALL_MACROS_CATEGORY_UUID), FILE_SYSTEM(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID), - IDENTITY(DataStorage.ALL_IDENTITIES_CATEGORY_UUID); + IDENTITY(DataStorage.ALL_IDENTITIES_CATEGORY_UUID), + NETWORK(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID); private final UUID category; } diff --git a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java index bfe9b7f1f..6be83ad59 100644 --- a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java @@ -9,8 +9,10 @@ import io.xpipe.app.process.CommandControl; import io.xpipe.app.process.ShellControl; import io.xpipe.app.process.ShellDialect; import io.xpipe.app.secret.SecretRetrievalStrategy; +import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.RemoteDesktopDockContentEntry; +import io.xpipe.app.util.HttpProxy; import io.xpipe.app.vnc.VncBaseStore; import io.xpipe.core.SecretValue; @@ -19,6 +21,7 @@ import javafx.beans.property.Property; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import java.util.ServiceLoader; public abstract class ProcessControlProvider { @@ -82,4 +85,6 @@ public abstract class ProcessControlProvider { public abstract void cloneRepository(String url, Path target) throws Exception; public abstract void pullRepository(Path target) throws Exception; + + public abstract Optional getHttpProxy(DataStoreEntryRef store) throws Exception; } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationMenu.java index b2b62a097..e9461e92b 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationMenu.java @@ -101,7 +101,7 @@ public class StoreCreationMenu { menu.getItems() .add(categoryMenu( "addOther", - "mdi2f-folder-plus-outline", null, DataStoreCreationCategory.CLUSTER, DataStoreCreationCategory.FILE_SYSTEM, DataStoreCreationCategory.SERIAL)); + "mdi2f-folder-plus-outline", null, DataStoreCreationCategory.NETWORK, DataStoreCreationCategory.CLUSTER, DataStoreCreationCategory.FILE_SYSTEM, DataStoreCreationCategory.SERIAL)); menu.getItems().add(new SeparatorMenuItem()); diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java index 529c822c8..1af6fd453 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java @@ -24,6 +24,8 @@ import javafx.scene.layout.VBox; import lombok.Getter; +import java.util.Objects; + import static atlantafx.base.theme.Styles.ACCENT; import static atlantafx.base.theme.Styles.BUTTON_OUTLINED; @@ -94,12 +96,16 @@ public class ErrorHandlerComp extends SimpleRegionBuilder { private String getEventDescription() { var desc = event.getDescription(); + String lastLine = desc; Throwable t = event.getThrowable(); while (t != null) { var toAppend = t.getMessage() != null ? t.getMessage() : AppI18n.get("errorTypeOccured", t.getClass().getSimpleName()); - desc = desc != null ? desc + "\n\n" + toAppend : toAppend; + if (!Objects.equals(toAppend, lastLine)) { + desc = desc != null ? desc + "\n\n" + toAppend : toAppend; + lastLine = toAppend; + } t = t.getCause() != t && !(t instanceof ProcessOutputException) ? t.getCause() : null; } 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 c906a589d..b4745e442 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -100,6 +100,12 @@ public final class AppPrefs { .valueClass(Boolean.class) .requiresRestart(false) .build()); + final BooleanProperty useExternalNetcatForProxies = map(Mapping.builder() + .property(new GlobalBooleanProperty(false)) + .key("useExternalNetcatForProxies") + .valueClass(Boolean.class) + .requiresRestart(false) + .build()); final BooleanProperty pinLocalMachineOnStartup = map(Mapping.builder() .property(new GlobalBooleanProperty(false)) .key("pinLocalMachineOnStartup") @@ -275,6 +281,12 @@ public final class AppPrefs { .valueClass(ShellScript.class) .log(false) .build()); + final Property httpProxy = map(Mapping.builder() + .property(new GlobalObjectProperty<>()) + .key("httpProxy") + .valueClass(UUID.class) + .requiresRestart(false) + .build()); final Property terminalProxy = map(Mapping.builder() .property(new GlobalObjectProperty<>()) .key("terminalProxy") @@ -443,6 +455,7 @@ public final class AppPrefs { new ApiCategory(), new McpCategory(), new UpdatesCategory(), + new HttpProxyCategory(), new SecurityCategory(), new WorkspacesCategory(), new DeveloperCategory(), @@ -465,6 +478,10 @@ public final class AppPrefs { return globalStorageHandler.isInitialized(); } + public ObservableBooleanValue useExternalNetcatForProxies() { + return useExternalNetcatForProxies; + } + public ObservableValue disableHttpsTlsCheck() { return disableHttpsTlsCheck; } @@ -585,6 +602,10 @@ public final class AppPrefs { return apiKey; } + public ObservableValue httpProxy() { + return httpProxy; + } + public ObservableBooleanValue disableApiAuthentication() { return disableApiAuthentication; } diff --git a/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java b/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java new file mode 100644 index 000000000..aec97787f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java @@ -0,0 +1,110 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.comp.BaseRegionBuilder; +import io.xpipe.app.comp.RegionBuilder; +import io.xpipe.app.comp.base.*; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.DataStore; +import io.xpipe.app.ext.DataStoreCreationCategory; +import io.xpipe.app.ext.DataStoreProviders; +import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.hub.comp.StoreChoiceComp; +import io.xpipe.app.hub.comp.StoreCreationDialog; +import io.xpipe.app.hub.comp.StoreCreationModel; +import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.platform.OptionsBuilder; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.terminal.*; +import io.xpipe.app.util.DocumentationLink; +import io.xpipe.app.util.HttpProxy; +import io.xpipe.core.OsType; +import javafx.beans.property.SimpleObjectProperty; + +public class HttpProxyCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "httpProxy"; + } + + @Override + protected LabelGraphic getIcon() { + return new LabelGraphic.IconGraphic("mdi2s-server-network-outline"); + } + + @Override + protected BaseRegionBuilder create() { + var prefs = AppPrefs.get(); + return new OptionsBuilder() + .title("httpProxyConfiguration") + .sub(proxy()) + .sub(new OptionsBuilder() + .pref(prefs.disableHttpsTlsCheck) + .addToggle(prefs.disableHttpsTlsCheck) + ) + .buildComp(); + } + + private OptionsBuilder proxy() { + var prefs = AppPrefs.get(); + var initial = prefs.httpProxy.getValue(); + var initialRef = initial != null ? DataStorage.get().getStoreEntryIfPresent(initial) + .map(e -> e.ref()) + .filter(e -> HttpProxy.canUseAsProxy(e)) + .orElse(null) : null; + var ref = new SimpleObjectProperty<>(initialRef); + ref.addListener((observable, oldValue, newValue) -> { + prefs.httpProxy.setValue( + newValue != null + ? newValue.get().getUuid() + : null); + }); + var proxyChoice = new DelayedInitComp( + RegionBuilder.of(() -> { + var comp = new StoreChoiceComp<>( + null, + ref, + DataStore.class, + r -> HttpProxy.canUseAsProxy(r), + StoreViewState.get().getAllConnectionsCategory()) { + @Override + protected String toName(DataStoreEntry entry) { + if (entry == null) { + return AppI18n.get("systemDefault"); + } + + return super.toName(entry); + } + + @Override + protected String toGraphic(DataStoreEntry entry) { + if (entry == null) { + return "proc:networkProxy_icon.svg"; + } + + return super.toGraphic(entry); + } + }; + return comp.build(); + }), + () -> StoreViewState.get() != null && StoreViewState.get().isInitialized()); + proxyChoice.maxWidth(getCompWidth()); + + var addButton = new ButtonComp(AppI18n.observable("addProxy"), () -> { + var selected = DataStoreProviders.byId("networkProxy").orElseThrow(); + StoreCreationDialog.showCreation( + null, selected.defaultStore(DataStorage.get().getSelectedCategory()), + DataStoreCreationCategory.NETWORK, + ignored -> {}, + false); + }); + + return new OptionsBuilder() + .nameAndDescription("httpProxy") + .addComp(proxyChoice, ref) + .addComp(addButton); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/McpCategory.java b/app/src/main/java/io/xpipe/app/prefs/McpCategory.java index f240d7e62..a46865c70 100644 --- a/app/src/main/java/io/xpipe/app/prefs/McpCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/McpCategory.java @@ -138,7 +138,7 @@ public class McpCategory extends AppPrefsCategory { .pref(prefs.enableMcpServer) .addToggle(prefs.enableMcpServer) .nameAndDescription("mcpClientConfigurationDetails") - .addComp(tabComp) + .addComp(tabComp.maxWidth(getCompWidth())) .pref(prefs.enableMcpMutationTools) .addToggle(prefs.enableMcpMutationTools) .hide(prefs.enableMcpServer.not()) @@ -150,7 +150,7 @@ public class McpCategory extends AppPrefsCategory { .getTextArea() .promptTextProperty() .bind(AppI18n.observable("mcpAdditionalContextSample")); - })) + }).maxWidth(getCompWidth())) .hide(prefs.enableMcpServer.not())) .buildComp(); } diff --git a/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java b/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java index 8915670a1..fd4857e76 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java @@ -35,8 +35,7 @@ public class SecurityCategory extends AppPrefsCategory { .addToggle(prefs.dontAutomaticallyStartVmSshServer) .pref(prefs.disableTerminalRemotePasswordPreparation) .addToggle(prefs.disableTerminalRemotePasswordPreparation) - .pref(prefs.disableHttpsTlsCheck) - .addToggle(prefs.disableHttpsTlsCheck)); + ); return builder.buildComp(); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/SshCategory.java b/app/src/main/java/io/xpipe/app/prefs/SshCategory.java index e40d5c6cc..fb757ad03 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SshCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SshCategory.java @@ -34,6 +34,11 @@ public class SshCategory extends AppPrefsCategory { options.addComp(prefs.getCustomOptions("x11WslInstance").buildComp()); } + options.sub(new OptionsBuilder() + .pref(prefs.useExternalNetcatForProxies) + .addToggle(prefs.useExternalNetcatForProxies) + ); + var agentTest = new SshAgentTestComp( () -> {}, new SimpleObjectProperty<>(CustomAgentStrategy.builder().build())); diff --git a/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java index 6d23745e7..70e435ef6 100644 --- a/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/HashicorpVaultPasswordManager.java @@ -190,9 +190,7 @@ public class HashicorpVaultPasswordManager implements PasswordManager { } var res = HttpHelper.client().send(req.build(), HttpResponse.BodyHandlers.ofString()); - if (res.statusCode() >= 400) { - throw new IOException(res.body()); - } + HttpHelper.checkOrThrow(res); var resJson = JacksonMapper.getDefault().readTree(res.body()); if (!resJson.isObject()) { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java b/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java index c236a1292..6daccc3a3 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageGroupStrategy.java @@ -265,9 +265,7 @@ public interface DataStorageGroupStrategy { .POST(java.net.http.HttpRequest.BodyPublishers.noBody()) .build(); var result = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofString()); - if (result.statusCode() >= 400) { - throw ErrorEventFactory.expected(new IOException(result.body())); - } + HttpHelper.checkOrThrow(result); var body = result.body(); if (body.length() == 0) { throw ErrorEventFactory.expected(new IllegalArgumentException("Http response body is empty")); diff --git a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java index c6e20359f..04669566e 100644 --- a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java @@ -51,7 +51,7 @@ public class MobaXTermTerminalType implements ExternalApplicationType.WindowsTyp var rawCommand = command.buildSimple(); var script = AppLocalTemp.getLocalTempDataDirectory().resolve("mobaxpipe.sh"); Files.writeString(Path.of(script.toString()), "#!/usr/bin/env bash\n" + rawCommand); - var fixedFile = script.toString().replaceAll("\\\\", "/").replaceAll("\\s", "\\$0"); + var fixedFile = script.toString().replaceAll("\\\\", "/").replaceAll("\\s", "\\\\$0"); launch(CommandBuilder.of().add("-newtab").add(fixedFile)); } diff --git a/app/src/main/java/io/xpipe/app/update/AppDownloads.java b/app/src/main/java/io/xpipe/app/update/AppDownloads.java index 47e449fbf..a401d5257 100644 --- a/app/src/main/java/io/xpipe/app/update/AppDownloads.java +++ b/app/src/main/java/io/xpipe/app/update/AppDownloads.java @@ -31,9 +31,7 @@ public class AppDownloads { var httpRequest = builder.uri(URI.create(release.getUrl())).GET().build(); var client = HttpHelper.client(); var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); - if (response.statusCode() >= 400) { - throw new IOException(new String(response.body(), StandardCharsets.UTF_8)); - } + HttpHelper.checkOrThrow(response); var downloadFile = AppCache.getBasePath().resolve(release.getFile()); Files.write(downloadFile, response.body()); @@ -64,10 +62,7 @@ public class AppDownloads { var httpRequest = builder.uri(uri).GET().build(); var client = HttpHelper.client(); var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() >= 400) { - var s = response.body(); - throw new IOException("Changelog not found" + (s != null && !s.isEmpty() ? ": " + s : "")); - } + HttpHelper.checkOrThrow(response); var json = JacksonMapper.getDefault().readTree(response.body()); var changelog = json.required("changelog").asText(); return changelog; @@ -104,9 +99,7 @@ public class AppDownloads { .build(); var client = HttpHelper.client(); var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() >= 400) { - throw new IOException(response.body()); - } + HttpHelper.checkOrThrow(response); var dateEntry = response.headers().firstValue("Date"); if (dateEntry.isPresent()) { diff --git a/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java b/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java index a3de072b9..e7ebfcf4c 100644 --- a/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java +++ b/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java @@ -28,9 +28,7 @@ public class GithubReleaseDownloader { .uri(URI.create(getDownloadUrl(repository, filter))) .build(); var r = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofByteArray()); - if (r.statusCode() >= 400) { - throw new IOException(new String(r.body(), StandardCharsets.UTF_8)); - } + HttpHelper.checkOrThrow(r); Files.createDirectories(tempDir); Files.write(temp, r.body()); @@ -55,9 +53,7 @@ public class GithubReleaseDownloader { .uri(URI.create("https://api.github.com/repos/" + repository + "/releases")) .build(); var r = HttpHelper.client().send(request, HttpResponse.BodyHandlers.ofString()); - if (r.statusCode() >= 400) { - throw new IOException(r.body()); - } + HttpHelper.checkOrThrow(r); var json = JacksonMapper.getDefault().readTree(r.body()); var latest = json.get(0); 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 d34606a2c..46e2d1913 100644 --- a/app/src/main/java/io/xpipe/app/util/HttpHelper.java +++ b/app/src/main/java/io/xpipe/app/util/HttpHelper.java @@ -1,10 +1,18 @@ package io.xpipe.app.util; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.issue.ErrorAction; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import lombok.SneakyThrows; +import java.io.IOException; +import java.net.*; import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.security.cert.X509Certificate; import javax.net.ssl.SSLContext; @@ -15,10 +23,17 @@ public class HttpHelper { @SneakyThrows public static HttpClient client() { + var proxy = HttpProxy.getActiveProxy(); + return client(proxy.orElse(null), AppPrefs.get() != null && AppPrefs.get().disableHttpsTlsCheck().getValue()); + } + + @SneakyThrows + public static HttpClient client(HttpProxy proxy, boolean checkTls) { var builder = HttpClient.newBuilder(); builder.version(HttpClient.Version.HTTP_1_1); builder.followRedirects(HttpClient.Redirect.NORMAL); - if (AppPrefs.get() != null && AppPrefs.get().disableHttpsTlsCheck().getValue()) { + + if (!checkTls) { var sslContext = SSLContext.getInstance("TLS"); var trustManager = new X509TrustManager() { @Override @@ -35,6 +50,62 @@ public class HttpHelper { sslContext.init(null, new TrustManager[] {trustManager}, new SecureRandom()); builder.sslContext(sslContext); } + + if (proxy != null) { + builder.proxy(ProxySelector.of(new InetSocketAddress(proxy.getHost(), proxy.getPort()))); + builder.authenticator(new Authenticator() { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + if (proxy.getUser() != null && proxy.getPassword() != null) { + return new PasswordAuthentication(proxy.getUser(), proxy.getPassword().getSecret()); + } else { + return null; + } + } + }); + } + return builder.build(); } + + public static void checkOrThrow(HttpResponse res) throws IOException { + if (res.statusCode() == 407) { + var ex = new IOException("HTTP proxy authentication required"); + ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(ex) + .expected() + .customAction(new ErrorAction() { + @Override + public String getName() { + return AppI18n.get("httpProxyError"); + } + + @Override + public String getDescription() { + return AppI18n.get("httpProxyErrorDescription"); + } + + @Override + public boolean handle(ErrorEvent event) throws Exception { + AppPrefs.get().selectCategory("httpProxy"); + return true; + } + })); + throw ex; + } + + if (res.statusCode() >= 400) { + if (res.body() instanceof String s) { + var msg = !s.isEmpty() ? + s : + "Received HTTP " + res.statusCode() + " without further details"; + throw new IOException(msg); + } else if (res.body() instanceof byte[] b) { + var msg = b.length > 0 ? + new String(b, StandardCharsets.UTF_8) : + "Received HTTP " + res.statusCode() + " without further details"; + throw new IOException(msg); + } + } + } } diff --git a/app/src/main/java/io/xpipe/app/util/HttpProxy.java b/app/src/main/java/io/xpipe/app/util/HttpProxy.java new file mode 100644 index 000000000..e849db770 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/HttpProxy.java @@ -0,0 +1,59 @@ +package io.xpipe.app.util; + +import io.xpipe.app.ext.DataStore; +import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.SecretValue; +import lombok.Value; + +import java.util.Optional; + +@Value +public class HttpProxy { + + public static Optional getActiveProxy() { + if (AppPrefs.get() == null) { + return Optional.empty(); + } + + var current = AppPrefs.get().httpProxy().getValue(); + if (current == null) { + return Optional.empty(); + } + + var found = DataStorage.get().getStoreEntryIfPresent(current); + if (found.isEmpty()) { + return Optional.empty(); + } + + try { + var proxy = ProcessControlProvider.get().getHttpProxy(found.get().ref()); + return proxy; + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); + return Optional.empty(); + } + } + + public static boolean canUseAsProxy(DataStoreEntryRef ref) { + if (!ref.get().getValidity().isUsable()) { + return false; + } + + try { + return ProcessControlProvider.get().getHttpProxy(ref).isPresent(); + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); + return false; + } + } + + String host; + int port; + String user; + SecretValue password; +} diff --git a/build.gradle b/build.gradle index 52506f94f..dc1835ca9 100644 --- a/build.gradle +++ b/build.gradle @@ -168,8 +168,13 @@ def getJvmArgs() { // Why is this not on by default? ... // https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/net/doc-files/net-properties.html + // https://stackoverflow.com/questions/53333556/proxy-authentication-with-jdk-11-httpclient + // https://stackoverflow.com/questions/75150081/ioexception-too-many-authentication-attempts-limit-3-when-using-jdk-httpcli jvmRunArgs += [ - '-Djava.net.useSystemProxies=true' + '-Djava.net.useSystemProxies=true', + '-Djdk.http.auth.proxying.disabledSchemes=""', + '-Djdk.http.auth.tunneling.disabledSchemes=""', + '-Djdk.httpclient.auth.retrylimit=1' ] // Fix platform theme detection on macOS diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTextSource.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTextSource.java index 226256467..20f102c10 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTextSource.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTextSource.java @@ -177,9 +177,7 @@ public interface ScriptTextSource { var req = HttpRequest.newBuilder().GET().uri(URI.create(url)).build(); var r = HttpHelper.client().send(req, HttpResponse.BodyHandlers.ofString()); - if (r.statusCode() >= 400) { - throw ErrorEventFactory.expected(new IOException(r.body())); - } + HttpHelper.checkOrThrow(r); Files.createDirectories(path.getParent()); Files.writeString(path, r.body()); diff --git a/img/proc/networkProxy_icon-dark.svg b/img/proc/networkProxy_icon-dark.svg new file mode 100644 index 000000000..917a2974c --- /dev/null +++ b/img/proc/networkProxy_icon-dark.svg @@ -0,0 +1,55 @@ + +identityidentity diff --git a/img/proc/networkProxy_icon.svg b/img/proc/networkProxy_icon.svg new file mode 100644 index 000000000..367aac49f --- /dev/null +++ b/img/proc/networkProxy_icon.svg @@ -0,0 +1,55 @@ + +identityidentity diff --git a/lang/strings/fixed_en.properties b/lang/strings/fixed_en.properties index 47eae7b7f..5253e2005 100644 --- a/lang/strings/fixed_en.properties +++ b/lang/strings/fixed_en.properties @@ -168,4 +168,5 @@ protonPassPasswordPlaceholder=Vault Name/Item name protonPass=Proton Pass passwork=Passwork passworkPlaceholder=Item ID +socks5=SOCKS5 diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 106577491..ca6435212 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1237,6 +1237,8 @@ customIpDescription=Override the default local VM IP detection if you use advanc automaticallyDetect=Automatically detect userAddDialogTitle=User creation groupAddDialogTitle=Group creation +keyPassphrase=Key passphrase +keyPassphraseDescription=The optional passphrase for your key passphrase=Passphrase repeatPassphrase=Repeat passphrase groupSecret=Group secret @@ -2077,3 +2079,24 @@ rdpWindowsSecurityWarningDialogTitle=RDP security warnings rdpWindowsSecurityWarningDialogContent=Since Windows build 26200, Windows will now always show of warning when an unsigned RDP file is launched, regardless of its contents.\n\nYou can disable this warning in the settings menu. openSettings=Open settings 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 +networkProxyType=Proxy type +networkProxyTypeDescription=The protocol of the proxy server +networkProxyHost=Host +networkProxyHostDescription=The host the proxy server is running on +networkProxyPort=Port +networkProxyPortDescription=The port the proxy server is listening on +networkProxyUsername=Username +networkProxyUsernameDescription=The username to log into the proxy if authentication is required +networkProxyPassword=Password +networkProxyPasswordDescription=The password to log into the proxy if authentication is required +httpProxy=HTTP proxy +httpProxyDescription=The proxy to use for any kind of HTTP requests sent by XPipe, e.g. update checks and license check requests. +httpProxyConfiguration=HTTP proxy configuration +addProxy=Add proxy ... +httpProxyError=Configure HTTP proxy settings +httpProxyErrorDescription=Add HTTP proxy authentication details in the settings menu +systemDefault=System default diff --git a/version b/version index c8c9e62b1..4773f3e0d 100644 --- a/version +++ b/version @@ -1 +1 @@ -22.11-1 +23.0-1