Properly implement launch command

This commit is contained in:
crschnick 2025-02-24 13:43:49 +00:00
parent cdfd7dc67d
commit 8f653880ba
9 changed files with 288 additions and 132 deletions

View file

@ -20,7 +20,7 @@ It currently supports:
- [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines
- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers
- [Tailscale](https://docs.xpipe.io/guide/tailscale) and [Teleport](https://docs.xpipe.io/guide/teleport) connections
- Windows Subsystem for Linux, Cygwin, and MSYS2 instances
- Windows Subsystem for Linux, Cygwin, and MSYS2 environments
- Powershell Remote Sessions
- RDP and VNC connections
@ -170,7 +170,9 @@ as it is not a perfect standalone version. It should however run on most systems
## Docker container
XPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe. Since it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured.
XPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe.
Since it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured.
# Further information

View file

@ -20,7 +20,9 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
public class AppBeaconServer {
@ -34,6 +36,7 @@ public class AppBeaconServer {
private final boolean propertyPort;
private boolean running;
private ExecutorService executor;
private HttpServer server;
@Getter
@ -105,7 +108,11 @@ public class AppBeaconServer {
}
running = false;
server.stop(1);
server.stop(0);
executor.shutdown();
try {
executor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {}
}
private void initAuthSecret() throws IOException {
@ -127,12 +134,7 @@ public class AppBeaconServer {
}
private void start() throws IOException {
server = HttpServer.create(
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
});
server.setExecutor(Executors.newFixedThreadPool(5, r -> {
executor = Executors.newFixedThreadPool(5, r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setName("http handler");
@ -140,7 +142,13 @@ public class AppBeaconServer {
ErrorEvent.fromThrowable(e).handle();
});
return t;
}));
});
server = HttpServer.create(
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
});
server.setExecutor(executor);
var resourceMap = Map.of(
"openapi.yaml", "misc/openapi.yaml",

View file

@ -1,6 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.api.ConnectionQueryExchange;
@ -14,43 +15,7 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
var catMatcher = Pattern.compile(
toRegex("all connections/" + msg.getCategoryFilter().toLowerCase()));
var conMatcher = Pattern.compile(toRegex(msg.getConnectionFilter().toLowerCase()));
var typeMatcher = Pattern.compile(toRegex(msg.getTypeFilter().toLowerCase()));
List<DataStoreEntry> found = new ArrayList<>();
for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {
if (!storeEntry.getValidity().isUsable()) {
continue;
}
var name = DataStorage.get().getStorePath(storeEntry).toString();
if (!conMatcher.matcher(name).matches()) {
continue;
}
var cat = DataStorage.get()
.getStoreCategoryIfPresent(storeEntry.getCategoryUuid())
.orElse(null);
if (cat == null) {
continue;
}
var c = DataStorage.get().getStorePath(cat).toString();
if (!catMatcher.matcher(c).matches()) {
continue;
}
if (!typeMatcher
.matcher(storeEntry.getProvider().getId().toLowerCase())
.matches()) {
continue;
}
found.add(storeEntry);
}
var found = DataStorageQuery.query(msg.getCategoryFilter(), msg.getConnectionFilter(), msg.getTypeFilter());
return Response.builder()
.found(found.stream().map(entry -> entry.getUuid()).toList())
.build();
@ -60,85 +25,4 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
public Object getSynchronizationObject() {
return DataStorage.get();
}
private String toRegex(String pattern) {
// https://stackoverflow.com/a/17369948/6477761
StringBuilder sb = new StringBuilder(pattern.length());
int inGroup = 0;
int inClass = 0;
int firstIndexInClass = -1;
char[] arr = pattern.toCharArray();
for (int i = 0; i < arr.length; i++) {
char ch = arr[i];
switch (ch) {
case '\\':
if (++i >= arr.length) {
sb.append('\\');
} else {
char next = arr[i];
switch (next) {
case ',':
// escape not needed
break;
case 'Q':
case 'E':
// extra escape needed
sb.append('\\');
default:
sb.append('\\');
}
sb.append(next);
}
break;
case '*':
if (inClass == 0) sb.append(".*");
else sb.append('*');
break;
case '?':
if (inClass == 0) sb.append('.');
else sb.append('?');
break;
case '[':
inClass++;
firstIndexInClass = i + 1;
sb.append('[');
break;
case ']':
inClass--;
sb.append(']');
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
if (inClass == 0 || (firstIndexInClass == i && ch == '^')) sb.append('\\');
sb.append(ch);
break;
case '!':
if (firstIndexInClass == i) sb.append('^');
else sb.append('!');
break;
case '{':
inGroup++;
sb.append('(');
break;
case '}':
inGroup--;
sb.append(')');
break;
case ',':
if (inGroup > 0) sb.append('|');
else sb.append(',');
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
}

View file

@ -0,0 +1,45 @@
package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
var found = DataStorageQuery.queryUserInput(msg.getConnection());
if (found.isEmpty()) {
throw new BeaconClientException("No connection found for input " + msg.getConnection());
}
if (found.size() > 1) {
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
}
var e = found.getFirst();
var isShell = e.getStore() instanceof ShellStore;
if (!isShell) {
throw new BeaconClientException("Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
}
var r = TerminalLauncherManager.externalExchange(e.ref());
return Response.builder().command(r).build();
}
@Override
public boolean requiresEnabledApi() {
return false;
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -0,0 +1,148 @@
package io.xpipe.app.storage;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.beacon.api.ConnectionQueryExchange;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class DataStorageQuery {
public static List<DataStoreEntry> queryUserInput(String connection) {
var found = query("*", "*" + connection + "*", "*");
if (found.size() > 1) {
var narrow = found.stream().filter(dataStoreEntry -> dataStoreEntry.getName().equalsIgnoreCase(connection)).toList();
if (narrow.size() == 1) {
return narrow;
}
}
return found;
}
public static List<DataStoreEntry> query(String categoryFilter, String connectionFilter, String typeFilter) {
var catMatcher = Pattern.compile(
toRegex("all connections/" + categoryFilter.toLowerCase()));
var conMatcher = Pattern.compile(toRegex(connectionFilter.toLowerCase()));
var typeMatcher = Pattern.compile(toRegex(typeFilter.toLowerCase()));
List<DataStoreEntry> found = new ArrayList<>();
for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {
if (!storeEntry.getValidity().isUsable()) {
continue;
}
var name = DataStorage.get().getStorePath(storeEntry).toString();
if (!conMatcher.matcher(name).matches()) {
continue;
}
var cat = DataStorage.get()
.getStoreCategoryIfPresent(storeEntry.getCategoryUuid())
.orElse(null);
if (cat == null) {
continue;
}
var c = DataStorage.get().getStorePath(cat).toString();
if (!catMatcher.matcher(c).matches()) {
continue;
}
if (!typeMatcher
.matcher(storeEntry.getProvider().getId().toLowerCase())
.matches()) {
continue;
}
found.add(storeEntry);
}
return found;
}
private static String toRegex(String pattern) {
pattern = pattern.replaceAll("\\*\\*", "#");
// https://stackoverflow.com/a/17369948/6477761
StringBuilder sb = new StringBuilder(pattern.length());
int inGroup = 0;
int inClass = 0;
int firstIndexInClass = -1;
char[] arr = pattern.toCharArray();
for (int i = 0; i < arr.length; i++) {
char ch = arr[i];
switch (ch) {
case '\\':
if (++i >= arr.length) {
sb.append('\\');
} else {
char next = arr[i];
switch (next) {
case ',':
// escape not needed
break;
case 'Q':
case 'E':
// extra escape needed
sb.append('\\');
default:
sb.append('\\');
}
sb.append(next);
}
break;
case '*':
if (inClass == 0) sb.append("[^/]*");
else sb.append('*');
break;
case '#':
if (inClass == 0) sb.append(".*");
else sb.append('*');
break;
case '?':
if (inClass == 0) sb.append('.');
else sb.append('?');
break;
case '[':
inClass++;
firstIndexInClass = i + 1;
sb.append('[');
break;
case ']':
inClass--;
sb.append(']');
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
if (inClass == 0 || (firstIndexInClass == i && ch == '^')) sb.append('\\');
sb.append(ch);
break;
case '!':
if (firstIndexInClass == i) sb.append('^');
else sb.append('!');
break;
case '{':
inGroup++;
sb.append('(');
break;
case '}':
inGroup--;
sb.append(')');
break;
case ',':
if (inGroup > 0) sb.append('|');
else sb.append(',');
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
}

View file

@ -1,17 +1,19 @@
package io.xpipe.app.terminal;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.SecretManager;
import io.xpipe.app.util.SecretQueryProgress;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.core.process.ProcessControl;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.TerminalInitScriptConfig;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.SequencedMap;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CountDownLatch;
public class TerminalLauncherManager {
@ -126,4 +128,33 @@ public class TerminalLauncherManager {
return ((TerminalLaunchResult.ResultSuccess) e.getResult()).getTargetScript();
}
}
public static List<String> externalExchange(DataStoreEntryRef<ShellStore> ref) throws BeaconClientException, BeaconServerException {
var request = UUID.randomUUID();
ShellControl session;
try {
session = ref.getStore().getOrStartSession();
} catch (Exception e) {
throw new BeaconServerException(e);
}
var config = TerminalInitScriptConfig.ofName(ref.get().getName());
submitAsync(request, session, config, null);
waitExchange(request);
var script = launchExchange(request);
try (var sc = LocalShell.getShell().start()) {
var runCommand = ProcessControlProvider.get().getEffectiveLocalDialect().getOpenScriptCommand(script.toString()).buildBaseParts(sc);
var cleaned = runCommand.stream().map(s -> {
if (s.startsWith("\"") && s.endsWith("\"")) {
s = s.substring(1, s.length() - 1);
} else if (s.startsWith("'") && s.endsWith("'")) {
s = s.substring(1, s.length() - 1);
}
return s;
}).toList();
return cleaned;
} catch (Exception e) {
throw new BeaconServerException(e);
}
}
}

View file

@ -151,6 +151,7 @@ open module io.xpipe.app {
TerminalPrepareExchangeImpl,
TerminalWaitExchangeImpl,
TerminalLaunchExchangeImpl,
TerminalExternalLaunchExchangeImpl,
SshLaunchExchangeImpl,
DaemonVersionExchangeImpl;
}

View file

@ -0,0 +1,36 @@
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 TerminalExternalLaunchExchange extends BeaconInterface<TerminalExternalLaunchExchange.Request> {
@Override
public String getPath() {
return "/terminal/externalLaunch";
}
@Jacksonized
@Builder
@Value
public static class Request {
@NonNull
String connection;
@NonNull
List<String> arguments;
}
@Jacksonized
@Builder
@Value
public static class Response {
@NonNull
List<String> command;
}
}

View file

@ -49,6 +49,7 @@ open module io.xpipe.beacon {
TerminalPrepareExchange,
TerminalWaitExchange,
TerminalLaunchExchange,
TerminalExternalLaunchExchange,
SshLaunchExchange,
FsReadExchange,
FsBlobExchange,