mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Implement basic ssh bridge
This commit is contained in:
parent
41f71d45a7
commit
20206b6263
8 changed files with 332 additions and 15 deletions
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -16,10 +16,7 @@ import io.xpipe.app.prefs.AppPrefs;
|
|||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.GitStorageHandler;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import io.xpipe.app.util.FileBridge;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.UnlockAlert;
|
||||
import io.xpipe.app.util.*;
|
||||
|
||||
public class BaseMode extends OperationMode {
|
||||
|
||||
|
@ -78,6 +75,7 @@ public class BaseMode extends OperationMode {
|
|||
public void finalTeardown() {
|
||||
TrackEvent.info("Background mode shutdown started");
|
||||
BrowserSessionModel.DEFAULT.reset();
|
||||
SshLocalBridge.reset();
|
||||
StoreViewState.reset();
|
||||
DataStoreProviders.reset();
|
||||
DataStorage.reset();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.ExternalApplicationType;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.util.CommandSupport;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.*;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
@ -16,13 +16,108 @@ import lombok.With;
|
|||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
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) {
|
||||
|
||||
@Override
|
||||
|
@ -652,6 +747,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
TabbyTerminalType.TABBY_WINDOWS,
|
||||
AlacrittyTerminalType.ALACRITTY_WINDOWS,
|
||||
WezTerminalType.WEZTERM_WINDOWS,
|
||||
TERMIUS,
|
||||
MOBAXTERM,
|
||||
CMD,
|
||||
PWSH,
|
||||
POWERSHELL);
|
||||
|
|
148
app/src/main/java/io/xpipe/app/util/SshLocalBridge.java
Normal file
148
app/src/main/java/io/xpipe/app/util/SshLocalBridge.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -7,20 +7,19 @@ import io.xpipe.core.process.ShellControl;
|
|||
import io.xpipe.core.process.TerminalInitScriptConfig;
|
||||
import io.xpipe.core.process.WorkingDirectoryFunction;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import lombok.Setter;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.NonFinal;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.SequencedMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
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(
|
||||
ProcessControl processControl, TerminalInitScriptConfig config, String directory, Entry entry) {
|
||||
|
@ -73,6 +72,15 @@ public class TerminalLauncherManager {
|
|||
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 {
|
||||
var e = entries.get(request);
|
||||
if (e == null) {
|
||||
|
@ -86,8 +94,8 @@ public class TerminalLauncherManager {
|
|||
}
|
||||
|
||||
var r = e.getResult();
|
||||
if (r instanceof ResultFailure failure) {
|
||||
entries.remove(request);
|
||||
if (r instanceof ResultFailure failure) {
|
||||
var t = failure.getThrowable();
|
||||
throw new BeaconServerException(t);
|
||||
}
|
||||
|
|
|
@ -154,5 +154,6 @@ open module io.xpipe.app {
|
|||
AskpassExchangeImpl,
|
||||
TerminalWaitExchangeImpl,
|
||||
TerminalLaunchExchangeImpl,
|
||||
SshLaunchExchangeImpl,
|
||||
DaemonVersionExchangeImpl;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ open module io.xpipe.beacon {
|
|||
AskpassExchange,
|
||||
TerminalWaitExchange,
|
||||
TerminalLaunchExchange,
|
||||
SshLaunchExchange,
|
||||
FsReadExchange,
|
||||
FsBlobExchange,
|
||||
FsWriteExchange,
|
||||
|
|
Loading…
Reference in a new issue