diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java index f912455f2..121b73c21 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -235,8 +235,8 @@ public class BrowserFileTransferOperation { } } - var noExt = target.getFileName().equals(target.getExtension()); - return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + target.getExtension())); + var ext = target.getExtension(); + return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : "")); } private void handleSingleAcrossFileSystems(FileEntry source) throws Exception { diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java index 9bf645f4b..96e159d92 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java @@ -85,7 +85,7 @@ public abstract class BrowserIconFileType { var name = entry.getPath().getFileName(); var ext = entry.getPath().getExtension(); - return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name); + return (ext.isPresent() && endings.contains("." + ext.get().toLowerCase(Locale.ROOT))) || endings.contains(name); } @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 e0b11130c..6ba41c7cb 100644 --- a/app/src/main/java/io/xpipe/app/util/DocumentationLink.java +++ b/app/src/main/java/io/xpipe/app/util/DocumentationLink.java @@ -41,6 +41,7 @@ public enum DocumentationLink { TUNNELS("guide/ssh-tunnels"), HYPERV("guide/hyperv"), SSH_MACS("guide/ssh#no-matching-mac-found"), + SSH_JUMP_SERVERS("guide/ssh#jump-servers"), KEEPASSXC("guide/password-manager#keepassxc"), PASSWORD_MANAGER("guide/password-manager"), VNC_CLIENTS("guide/vnc#external-clients"),; diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialects.java b/core/src/main/java/io/xpipe/core/process/ShellDialects.java index c24ecee25..c5b85ccda 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialects.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialects.java @@ -31,6 +31,7 @@ public class ShellDialects { public static ShellDialect CONSTRAINED_POWERSHELL; public static ShellDialect OVH_BASTION; public static ShellDialect HETZNER_BOX; + public static ShellDialect JUMP_SERVER; public static List getStartableDialects() { return ALL.stream() @@ -96,6 +97,7 @@ public class ShellDialects { CONSTRAINED_POWERSHELL = byId("constrainedPowershell"); OVH_BASTION = byId("ovhBastion"); HETZNER_BOX = byId("hetznerBox"); + JUMP_SERVER = byId("jumpServer"); } } } diff --git a/core/src/main/java/io/xpipe/core/process/ShellView.java b/core/src/main/java/io/xpipe/core/process/ShellView.java index 2f6385939..62d7d444f 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellView.java +++ b/core/src/main/java/io/xpipe/core/process/ShellView.java @@ -21,7 +21,8 @@ public class ShellView { public FilePath writeTextFileDeterministic(FilePath base, String text) throws Exception { var hash = Math.abs(text.hashCode()); - var target = FilePath.of(base.getBaseName().toString() + "-" + hash + "." + base.getExtension()); + var ext = base.getExtension(); + var target = FilePath.of(base.getBaseName().toString() + "-" + hash + (ext.isPresent() ? "." + ext.get() : "")); if (fileExists(target)) { return target; } diff --git a/core/src/main/java/io/xpipe/core/store/FilePath.java b/core/src/main/java/io/xpipe/core/store/FilePath.java index d627ad618..d31ef3174 100644 --- a/core/src/main/java/io/xpipe/core/store/FilePath.java +++ b/core/src/main/java/io/xpipe/core/store/FilePath.java @@ -6,10 +6,7 @@ import lombok.EqualsAndHashCode; import lombok.NonNull; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.regex.Pattern; public final class FilePath { @@ -156,13 +153,13 @@ public final class FilePath { return FilePath.of(value.substring(0, split)); } - public String getExtension() { - var name = FileNames.getFileName(value); + public Optional getExtension() { + var name = getFileName(); var split = name.split("\\."); - if (split.length == 0) { - return null; + if (split.length < 2) { + return Optional.empty(); } - return split[split.length - 1]; + return Optional.of(split[split.length - 1]); } public FilePath join(String... parts) { diff --git a/core/src/main/java/io/xpipe/core/util/KeyValue.java b/core/src/main/java/io/xpipe/core/util/KeyValue.java new file mode 100644 index 000000000..5f6ca4085 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/KeyValue.java @@ -0,0 +1,15 @@ +package io.xpipe.core.util; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Value +@Builder +@Jacksonized +@AllArgsConstructor +public class KeyValue { + String key; + String value; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java index dede23b73..3bdc8d1f7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java @@ -7,6 +7,8 @@ import io.xpipe.app.util.SecretRetrievalStrategy; import io.xpipe.app.util.Validators; import io.xpipe.core.process.*; import io.xpipe.core.store.FileNames; +import io.xpipe.core.store.FilePath; +import io.xpipe.core.util.KeyValue; import io.xpipe.core.util.ValidationException; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -18,6 +20,7 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import java.io.IOException; +import java.util.List; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -32,12 +35,15 @@ import java.io.IOException; @JsonSubTypes.Type(value = SshIdentityStrategy.OtherExternal.class) }) public interface SshIdentityStrategy { + default void checkComplete() throws ValidationException {} void prepareParent(ShellControl parent) throws Exception; void buildCommand(CommandBuilder builder); + List configOptions(ShellControl parent) throws Exception; + default SecretRetrievalStrategy getAskpassStrategy() { return new SecretRetrievalStrategy.None(); } @@ -50,9 +56,13 @@ public interface SshIdentityStrategy { public void prepareParent(ShellControl parent) {} @Override - public void buildCommand(CommandBuilder builder) { + public void buildCommand(CommandBuilder builder) {} + + @Override + public List configOptions(ShellControl parent) throws Exception { // Don't use any agent keys to prevent too many authentication failures - builder.add("-oIdentitiesOnly=yes").add("-oIdentityAgent=none").add("-oIdentityFile=none"); + return List.of(new KeyValue("IdentitiesOnly", "yes"), new KeyValue("IdentityAgent", "none"), new KeyValue("IdentityFile", "none"), + new KeyValue("PKCS11Provider", "none")); } } @@ -94,12 +104,11 @@ public interface SshIdentityStrategy { var pidEnvVariable = System.getenv("SSH_AGENT_PID"); return pidEnvVariable; }); + } - builder.add("-oIdentitiesOnly=no"); - builder.add("-oIdentityFile=none"); - if (forwardAgent) { - builder.add(1, "-A"); - } + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"), new KeyValue("IdentityFile", "none"), new KeyValue("PKCS11Provider", "none")); } } @@ -131,8 +140,6 @@ public interface SshIdentityStrategy { @Override public void buildCommand(CommandBuilder builder) { - builder.add("-oIdentitiesOnly=no"); - builder.add("-oIdentityFile=none"); builder.environment("SSH_AUTH_SOCK", parent -> { if (parent.getOsType().equals(OsType.WINDOWS)) { return getPageantWindowsPipe(parent); @@ -140,9 +147,11 @@ public interface SshIdentityStrategy { return null; }); - if (forwardAgent) { - builder.add(1, "-A"); - } + } + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"), new KeyValue("IdentityFile", "none"), new KeyValue("PKCS11Provider", "none")); } private String getPageantWindowsPipe(ShellControl parent) throws Exception { @@ -191,12 +200,11 @@ public interface SshIdentityStrategy { } @Override - public void buildCommand(CommandBuilder builder) { - builder.add("-oIdentitiesOnly=no"); - builder.add("-oIdentityFile=none"); - if (forwardAgent) { - builder.add(1, "-A"); - } + public void buildCommand(CommandBuilder builder) {} + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"), new KeyValue("IdentityFile", "none"), new KeyValue("PKCS11Provider", "none")); } } @@ -220,8 +228,6 @@ public interface SshIdentityStrategy { @Override public void buildCommand(CommandBuilder builder) { - builder.add("-oIdentitiesOnly=no"); - builder.add("-oIdentityFile=none"); builder.environment("SSH_AUTH_SOCK", sc -> { if (sc.getOsType() == OsType.WINDOWS) { return null; @@ -230,9 +236,11 @@ public interface SshIdentityStrategy { var r = sc.executeSimpleStringCommand("gpgconf --list-dirs agent-ssh-socket"); return r; }); - if (forwardAgent) { - builder.add(1, "-A"); - } + } + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"), new KeyValue("IdentityFile", "none"), new KeyValue("PKCS11Provider", "none")); } } @@ -304,29 +312,24 @@ public interface SshIdentityStrategy { } @Override - public void buildCommand(CommandBuilder builder) { - if (file == null) { - return; + public void buildCommand(CommandBuilder builder) {} + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "yes"), new KeyValue("IdentityAgent", "none"), + new KeyValue("IdentityFile", resolveFilePath(parent).toString()), new KeyValue("PKCS11Provider", "none")); + } + + private FilePath resolveFilePath(ShellControl sc) throws Exception { + var s = file.toAbsoluteFilePath(sc); + // The ~ is supported on all platforms, so manually replace it here for Windows + if (s.startsWith("~")) { + s = s.resolveTildeHome(sc.getOsType().getUserHomeDirectory(sc)); } - - builder.add("-i") - .add(sc -> { - if (sc == null) { - return "\"" + file.toAbsoluteFilePath(null) + "\""; - } - - var s = file.toAbsoluteFilePath(sc); - // The ~ is supported on all platforms, so manually replace it here for Windows - if (s.startsWith("~")) { - s = s.resolveTildeHome(sc.getOsType().getUserHomeDirectory(sc)); - } - var resolved = sc.getShellDialect() - .evaluateExpression(sc, s.toString()) - .readStdoutOrThrow(); - return sc.getShellDialect().fileArgument(resolved); - }) - .add("-oIdentitiesOnly=yes") - .add("-oIdentityAgent=none"); + var resolved = sc.getShellDialect() + .evaluateExpression(sc, s.toString()) + .readStdoutOrThrow(); + return FilePath.of(resolved); } @Override @@ -406,17 +409,14 @@ public interface SshIdentityStrategy { var path = sc.view().getLibraryPath(); builder.fixedEnvironment("LD_LIBRARY_PATH", dir + ":" + path); } - }) - .add("-oIdentityFile=none") - .add("-I") - .add(sc -> { - if (sc == null) { - return ""; - } - - return sc.getShellDialect().fileArgument(getFile(sc)); }); } + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("PKCS11Provider", getFile(parent)), + new KeyValue("IdentityFile", "none"), new KeyValue("IdentityAgent", "none")); + } } @Value @@ -450,8 +450,11 @@ public interface SshIdentityStrategy { builder.fixedEnvironment("LD_LIBRARY_PATH", dir + ":" + path); } }); - builder.add("-oIdentityFile=none"); - builder.add("-I").addFile(file); + } + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("PKCS11Provider", file), new KeyValue("IdentityFile", "none"), new KeyValue("IdentityAgent", "none")); } } @@ -467,12 +470,11 @@ public interface SshIdentityStrategy { public void prepareParent(ShellControl parent) {} @Override - public void buildCommand(CommandBuilder builder) { - if (forwardAgent) { - builder.add(1, "-A"); - } - builder.add("-oIdentityFile=none"); - builder.add("-oIdentitiesOnly=no"); + public void buildCommand(CommandBuilder builder) {} + + @Override + public List configOptions(ShellControl parent) throws Exception { + return List.of(new KeyValue("IdentitiesOnly", "no"), new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"), new KeyValue("IdentityFile", "none"), new KeyValue("PKCS11Provider", "none")); } } } diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index fc43891db..ee99cf4dd 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1470,3 +1470,5 @@ vncConnections=VNC connections passwordManagerIdentity=Password manager identity passwordManagerIdentity.displayName=Password manager identity passwordManagerIdentity.displayDescription=Retrieve the credentials of an identity from your password manager +useAsJumpServer=Jump server +useAsJumpServerDescription=This system is a jump server to be used with ProxyJump