Skip to content

Commit

Permalink
Implement basic ssh bridge
Browse files Browse the repository at this point in the history
  • Loading branch information
crschnick committed Aug 12, 2024
1 parent 41f71d4 commit 20206b6
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
}
6 changes: 2 additions & 4 deletions app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
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 {

Expand Down Expand Up @@ -78,6 +75,7 @@ public void onSwitchFrom() {}
public void finalTeardown() {
TrackEvent.info("Background mode shutdown started");
BrowserSessionModel.DEFAULT.reset();
SshLocalBridge.reset();
StoreViewState.reset();
DataStoreProviders.reset();
DataStorage.reset();
Expand Down
109 changes: 103 additions & 6 deletions app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,13 +16,108 @@
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
Expand Down Expand Up @@ -652,6 +747,8 @@ public TerminalInitFunction additionalInitCommands() {
TabbyTerminalType.TABBY_WINDOWS,
AlacrittyTerminalType.ALACRITTY_WINDOWS,
WezTerminalType.WEZTERM_WINDOWS,
TERMIUS,
MOBAXTERM,
CMD,
PWSH,
POWERSHELL);
Expand Down
148 changes: 148 additions & 0 deletions app/src/main/java/io/xpipe/app/util/SshLocalBridge.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 13 additions & 5 deletions app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@
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) {
Expand Down Expand Up @@ -73,6 +72,15 @@ public static CountDownLatch submitAsync(
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) {
Expand All @@ -86,8 +94,8 @@ public static Path waitForCompletion(UUID request) throws BeaconClientException,
}

var r = e.getResult();
entries.remove(request);
if (r instanceof ResultFailure failure) {
entries.remove(request);
var t = failure.getThrowable();
throw new BeaconServerException(t);
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,6 @@
AskpassExchangeImpl,
TerminalWaitExchangeImpl,
TerminalLaunchExchangeImpl,
SshLaunchExchangeImpl,
DaemonVersionExchangeImpl;
}
Loading

0 comments on commit 20206b6

Please sign in to comment.