Implement basic ssh bridge

This commit is contained in:
crschnick 2024-08-12 17:49:55 +00:00
parent 41f71d45a7
commit 20206b6263
8 changed files with 332 additions and 15 deletions

View file

@ -0,0 +1,32 @@
package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.beacon.api.SshLaunchExchange;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.StorePath;
import java.util.UUID;
public class SshLaunchExchangeImpl extends SshLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Exception {
if (msg.getStorePath() != null && !msg.getStorePath().contains("SSH_ORIGINAL_COMMAND")) {
var storePath = StorePath.create(msg.getStorePath());
var found = DataStorage.get().getStoreEntries().stream().filter(entry -> DataStorage.get().getStorePath(entry).equals(storePath)).findFirst();
if (found.isPresent() && found.get().getStore() instanceof ShellStore shellStore) {
TerminalLauncherManager.submitAsync(UUID.randomUUID(), shellStore.control(),
TerminalInitScriptConfig.ofName(DataStorage.get().getStoreEntryDisplayName(found.get())),null);
}
}
TerminalLauncherManager.submitAsync(UUID.randomUUID(), ((ShellStore) DataStorage.get().local().getStore()).control(),
TerminalInitScriptConfig.ofName("abc"),null);
var r = TerminalLauncherManager.waitForFirstLaunch();
var c = ProcessControlProvider.get().getEffectiveLocalDialect().getOpenScriptCommand(r.toString()).buildBaseParts(null);
return Response.builder().command(c).build();
}
}

View file

@ -16,10 +16,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.GitStorageHandler; import io.xpipe.app.storage.GitStorageHandler;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.*;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.UnlockAlert;
public class BaseMode extends OperationMode { public class BaseMode extends OperationMode {
@ -78,6 +75,7 @@ public class BaseMode extends OperationMode {
public void finalTeardown() { public void finalTeardown() {
TrackEvent.info("Background mode shutdown started"); TrackEvent.info("Background mode shutdown started");
BrowserSessionModel.DEFAULT.reset(); BrowserSessionModel.DEFAULT.reset();
SshLocalBridge.reset();
StoreViewState.reset(); StoreViewState.reset();
DataStoreProviders.reset(); DataStoreProviders.reset();
DataStorage.reset(); DataStorage.reset();

View file

@ -1,10 +1,10 @@
package io.xpipe.app.terminal; package io.xpipe.app.terminal;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.CommandSupport; import io.xpipe.app.util.*;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.*; import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableFunction; import io.xpipe.core.util.FailableFunction;
@ -16,13 +16,108 @@ import lombok.With;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.*;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
public interface ExternalTerminalType extends PrefsChoiceValue { public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType MOBAXTERM = new WindowsType("app.mobaXterm","MobaXterm") {
@Override
protected Optional<Path> determineInstallation() {
try {
var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Classes\\mobaxterm\\DefaultIcon");
return r.map(Path::of);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
return Optional.empty();
}
}
@Override
public boolean supportsTabs() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
try (var sc = LocalShell.getShell()) {
var fixedFile = configuration.getScriptFile().toString()
.replaceAll("\\\\", "/")
.replaceAll("\\s","\\$0");
var command = sc.getShellDialect() == ShellDialects.CMD ?
CommandBuilder.of().addQuoted("cmd /c " + fixedFile) :
CommandBuilder.of().addQuoted("powershell -NoProfile -ExecutionPolicy Bypass -File " + fixedFile);
sc.command(CommandBuilder.of().addFile(file.toString()).add("-newtab").add(command)).execute();
}
}
};
ExternalTerminalType TERMIUS = new ExternalTerminalType() {
@Override
public String getId() {
return "app.termius";
}
@Override
public boolean isAvailable() {
try (var sc = LocalShell.getShell()) {
return switch (OsType.getLocal()) {
case OsType.Linux linux -> {
yield CommandSupport.isInPathSilent(sc, "termius");
}
case OsType.MacOs macOs -> {
yield CommandSupport.isInPathSilent(sc, "termius");
}
case OsType.Windows windows -> {
var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_CURRENT_USER, "SOFTWARE\\Classes\\termius");
yield r.isPresent();
}
};
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
return false;
}
}
@Override
public boolean supportsTabs() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
SshLocalBridge.init();
var name = "xpipe_bridge";
var host = "localhost";
var port = 21722;
var user = System.getProperty("user.name");
Hyperlinks.open("termius://app/host-sharing#label=" + name + "&ip=" + host + "&port=" + port + "&username="
+ user + "&os=windows");
}
};
ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) { ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) {
@Override @Override
@ -652,6 +747,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
TabbyTerminalType.TABBY_WINDOWS, TabbyTerminalType.TABBY_WINDOWS,
AlacrittyTerminalType.ALACRITTY_WINDOWS, AlacrittyTerminalType.ALACRITTY_WINDOWS,
WezTerminalType.WEZTERM_WINDOWS, WezTerminalType.WEZTERM_WINDOWS,
TERMIUS,
MOBAXTERM,
CMD, CMD,
PWSH, PWSH,
POWERSHELL); POWERSHELL);

View file

@ -0,0 +1,148 @@
package io.xpipe.app.util;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.util.XPipeInstallation;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@Getter
public class SshLocalBridge {
private static SshLocalBridge INSTANCE;
private final Path directory;
private final int port;
private final String user;
@Setter
private ShellControl runningShell;
public SshLocalBridge(Path directory, int port, String user) {
this.directory = directory;
this.port = port;
this.user = user;
}
public Path getPubHostKey() {
return directory.resolve("host_key.pub");
}
public Path getHostKey() {
return directory.resolve("host_key");
}
public Path getPubIdentityKey() {
return directory.resolve("identity.pub");
}
public Path getIdentityKey() {
return directory.resolve("identity");
}
public Path getConfig() {
return directory.resolve("sshd_config");
}
public static void init() throws Exception {
if (INSTANCE != null) {
return;
}
try (var sc = LocalShell.getShell().start()) {
var bridgeDir = AppProperties.get().getDataDir().resolve("ssh_bridge");
Files.createDirectories(bridgeDir);
var port = AppBeaconServer.get().getPort() + 1;
var user = sc.getShellDialect().printUsernameCommand(sc).readStdoutOrThrow();
INSTANCE = new SshLocalBridge(bridgeDir, port, user);
var hostKey = INSTANCE.getHostKey();
if (!sc.getShellDialect().createFileExistsCommand(sc, hostKey.toString()).executeAndCheck()) {
sc.executeSimpleCommand("ssh-keygen -q -N \"\" -t ed25519 -f \"" + hostKey + "\"");
}
var idKey = INSTANCE.getIdentityKey();
if (!sc.getShellDialect().createFileExistsCommand(sc, idKey.toString()).executeAndCheck()) {
sc.executeSimpleCommand("ssh-keygen -q -N \"\" -t ed25519 -f \"" + idKey + "\"");
}
var config = INSTANCE.getConfig();
var command = "\"" + XPipeInstallation.getLocalDefaultCliExecutable() + "\" ssh-launch " + sc.getShellDialect().environmentVariable("SSH_ORIGINAL_COMMAND");
var pidFile = bridgeDir.resolve("sshd.pid");
var content = """
ForceCommand %s
PidFile "%s"
StrictModes no
SyslogFacility USER
LogLevel Debug3
Port %s
PasswordAuthentication no
HostKey "%s"
PubkeyAuthentication yes
AuthorizedKeysFile "%s"
"""
.formatted(command, pidFile.toString(), "" + port, INSTANCE.getHostKey().toString(), INSTANCE.getPubIdentityKey());;
Files.writeString(config, content);
INSTANCE.updateConfig();
var exec = getSshd(sc);
var launchCommand = CommandBuilder.of().addFile(exec).add("-f").addFile(INSTANCE.getConfig().toString()).add("-p", "" + port);
var control = ProcessControlProvider.get().createLocalProcessControl(true).start();
control.writeLine(launchCommand.buildFull(control));
INSTANCE.setRunningShell(control);
}
}
private void updateConfig() throws IOException {
var file = Path.of(System.getProperty("user.home"), ".ssh", "config");
if (!Files.exists(file)) {
return;
}
var content = Files.readString(file);
if (content.contains("xpipe_bridge")) {
return;
}
var updated = content + "\n\n" + """
Host xpipe_bridge
HostName localhost
User "%s"
Port %s
IdentityFile "%s"
""".formatted(port, user, getIdentityKey());
Files.writeString(file, updated);
}
private static String getSshd(ShellControl sc) throws Exception {
if (OsType.getLocal() == OsType.WINDOWS) {
return XPipeInstallation.getLocalBundledToolsDirectory().resolve("openssh").resolve("sshd").toString();
} else {
var exec = sc.executeSimpleStringCommand(sc.getShellDialect().getWhichCommand("sshd"));
return exec;
}
}
public static void reset() {
if (INSTANCE == null) {
return;
}
try {
INSTANCE.getRunningShell().closeStdin();
} catch (IOException e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
INSTANCE.getRunningShell().kill();
INSTANCE = null;
}
}

View file

@ -7,20 +7,19 @@ import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.TerminalInitScriptConfig; import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.process.WorkingDirectoryFunction; import io.xpipe.core.process.WorkingDirectoryFunction;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
import lombok.Setter; import lombok.Setter;
import lombok.Value; import lombok.Value;
import lombok.experimental.NonFinal; import lombok.experimental.NonFinal;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.LinkedHashMap;
import java.util.SequencedMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
public class TerminalLauncherManager { public class TerminalLauncherManager {
private static final Map<UUID, Entry> entries = new ConcurrentHashMap<>(); private static final SequencedMap<UUID, Entry> entries = new LinkedHashMap<>();
private static void prepare( private static void prepare(
ProcessControl processControl, TerminalInitScriptConfig config, String directory, Entry entry) { ProcessControl processControl, TerminalInitScriptConfig config, String directory, Entry entry) {
@ -73,6 +72,15 @@ public class TerminalLauncherManager {
return latch; return latch;
} }
public static Path waitForFirstLaunch() throws BeaconClientException, BeaconServerException {
if (entries.isEmpty()) {
throw new BeaconClientException("Unknown launch request");
}
var first = entries.firstEntry();
return waitForCompletion(first.getKey());
}
public static Path waitForCompletion(UUID request) throws BeaconClientException, BeaconServerException { public static Path waitForCompletion(UUID request) throws BeaconClientException, BeaconServerException {
var e = entries.get(request); var e = entries.get(request);
if (e == null) { if (e == null) {
@ -86,8 +94,8 @@ public class TerminalLauncherManager {
} }
var r = e.getResult(); var r = e.getResult();
entries.remove(request);
if (r instanceof ResultFailure failure) { if (r instanceof ResultFailure failure) {
entries.remove(request);
var t = failure.getThrowable(); var t = failure.getThrowable();
throw new BeaconServerException(t); throw new BeaconServerException(t);
} }

View file

@ -154,5 +154,6 @@ open module io.xpipe.app {
AskpassExchangeImpl, AskpassExchangeImpl,
TerminalWaitExchangeImpl, TerminalWaitExchangeImpl,
TerminalLaunchExchangeImpl, TerminalLaunchExchangeImpl,
SshLaunchExchangeImpl,
DaemonVersionExchangeImpl; DaemonVersionExchangeImpl;
} }

View file

@ -0,0 +1,32 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class SshLaunchExchange extends BeaconInterface<SshLaunchExchange.Request> {
@Override
public String getPath() {
return "/sshLaunch";
}
@Jacksonized
@Builder
@Value
public static class Request {
String storePath;
}
@Jacksonized
@Builder
@Value
public static class Response {
@NonNull
List<String> command;
}
}

View file

@ -47,6 +47,7 @@ open module io.xpipe.beacon {
AskpassExchange, AskpassExchange,
TerminalWaitExchange, TerminalWaitExchange,
TerminalLaunchExchange, TerminalLaunchExchange,
SshLaunchExchange,
FsReadExchange, FsReadExchange,
FsBlobExchange, FsBlobExchange,
FsWriteExchange, FsWriteExchange,