mirror of
https://github.com/xpipe-io/xpipe.git
synced 2025-04-17 09:43:37 +00:00
Properly implement launch command
This commit is contained in:
parent
cdfd7dc67d
commit
8f653880ba
9 changed files with 288 additions and 132 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
148
app/src/main/java/io/xpipe/app/storage/DataStorageQuery.java
Normal file
148
app/src/main/java/io/xpipe/app/storage/DataStorageQuery.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,6 +151,7 @@ open module io.xpipe.app {
|
|||
TerminalPrepareExchangeImpl,
|
||||
TerminalWaitExchangeImpl,
|
||||
TerminalLaunchExchangeImpl,
|
||||
TerminalExternalLaunchExchangeImpl,
|
||||
SshLaunchExchangeImpl,
|
||||
DaemonVersionExchangeImpl;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ open module io.xpipe.beacon {
|
|||
TerminalPrepareExchange,
|
||||
TerminalWaitExchange,
|
||||
TerminalLaunchExchange,
|
||||
TerminalExternalLaunchExchange,
|
||||
SshLaunchExchange,
|
||||
FsReadExchange,
|
||||
FsBlobExchange,
|
||||
|
|
Loading…
Add table
Reference in a new issue