diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index 4cc03a610..a5389921b 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -6,12 +6,15 @@ import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.beacon.BeaconConfig; import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.util.XPipeInstallation; import lombok.Getter; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.file.Files; import java.util.HashSet; import java.util.Set; +import java.util.UUID; import java.util.concurrent.Executors; public class AppBeaconServer { @@ -25,6 +28,10 @@ public class AppBeaconServer { private HttpServer server; @Getter private final Set sessions = new HashSet<>(); + @Getter + private final Set shellSessions = new HashSet<>(); + @Getter + private String localAuthSecret; static { int port; @@ -46,6 +53,7 @@ public class AppBeaconServer { public static void init() { try { + INSTANCE.initAuthSecret(); INSTANCE.start(); TrackEvent.withInfo("Started http server") .tag("port", INSTANCE.getPort()) @@ -85,6 +93,13 @@ public class AppBeaconServer { server.stop(1); } + private void initAuthSecret() throws IOException { + var file = XPipeInstallation.getLocalBeaconAuthFile(); + var id = UUID.randomUUID().toString(); + Files.writeString(file, id); + localAuthSecret = id; + } + private void start() throws IOException { server = HttpServer.create(new InetSocketAddress("localhost", port), 10); BeaconInterface.getAll().forEach(beaconInterface -> { diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java b/app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java new file mode 100644 index 000000000..a92415a7a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java @@ -0,0 +1,12 @@ +package io.xpipe.app.beacon; + +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.process.ShellControl; +import lombok.Value; + +@Value +public class BeaconShellSession { + + DataStoreEntry entry; + ShellControl control; +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FocusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java similarity index 80% rename from app/src/main/java/io/xpipe/app/beacon/impl/FocusExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java index 0497915f2..1a25af89e 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FocusExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java @@ -4,11 +4,11 @@ import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.FocusExchange; +import io.xpipe.beacon.api.DaemonFocusExchange; import java.io.IOException; -public class FocusExchangeImpl extends FocusExchange { +public class DaemonFocusExchangeImpl extends DaemonFocusExchange { @Override diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java similarity index 87% rename from app/src/main/java/io/xpipe/app/beacon/impl/ModeExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java index 0cb196637..ab7de9958 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ModeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java @@ -5,11 +5,11 @@ import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.util.ThreadHelper; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.ModeExchange; +import io.xpipe.beacon.api.DaemonModeExchange; import java.io.IOException; -public class ModeExchangeImpl extends ModeExchange { +public class DaemonModeExchangeImpl extends DaemonModeExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { // Wait for startup @@ -29,7 +29,7 @@ public class ModeExchangeImpl extends ModeExchange { } OperationMode.switchToSyncIfPossible(mode); - return ModeExchange.Response.builder() + return DaemonModeExchange.Response.builder() .usedMode(OperationMode.map(OperationMode.get())) .build(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/OpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java similarity index 87% rename from app/src/main/java/io/xpipe/app/beacon/impl/OpenExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java index 3109b1767..8a89878ec 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/OpenExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java @@ -6,11 +6,11 @@ import io.xpipe.app.launcher.LauncherInput; import io.xpipe.app.util.PlatformState; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.OpenExchange; +import io.xpipe.beacon.api.DaemonOpenExchange; import java.io.IOException; -public class OpenExchangeImpl extends OpenExchange { +public class DaemonOpenExchangeImpl extends DaemonOpenExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { if (msg.getArguments().isEmpty()) { diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/StatusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java similarity index 84% rename from app/src/main/java/io/xpipe/app/beacon/impl/StatusExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java index 883bf207c..77eadf156 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/StatusExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java @@ -5,11 +5,11 @@ import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.StatusExchange; +import io.xpipe.beacon.api.DaemonStatusExchange; import java.io.IOException; -public class StatusExchangeImpl extends StatusExchange { +public class DaemonStatusExchangeImpl extends DaemonStatusExchange { @Override public Object handle(HttpExchange exchange, Request body) throws IOException, BeaconClientException, BeaconServerException { diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/StopExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java similarity index 84% rename from app/src/main/java/io/xpipe/app/beacon/impl/StopExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java index ee272d356..0189350c2 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/StopExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java @@ -5,11 +5,11 @@ import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.util.ThreadHelper; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.StopExchange; +import io.xpipe.beacon.api.DaemonStopExchange; import java.io.IOException; -public class StopExchangeImpl extends StopExchange { +public class DaemonStopExchangeImpl extends DaemonStopExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/VersionExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java similarity index 87% rename from app/src/main/java/io/xpipe/app/beacon/impl/VersionExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java index d6fe83b97..f6ba986c4 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/VersionExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java @@ -4,11 +4,11 @@ import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.core.AppProperties; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.VersionExchange; +import io.xpipe.beacon.api.DaemonVersionExchange; import java.io.IOException; -public class VersionExchangeImpl extends VersionExchange { +public class DaemonVersionExchangeImpl extends DaemonVersionExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java index 9c5077cb5..a99ed993f 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java @@ -4,6 +4,8 @@ package io.xpipe.app.beacon.impl; import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.beacon.AppBeaconServer; import io.xpipe.app.beacon.BeaconSession; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.beacon.BeaconAuthMethod; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; import io.xpipe.beacon.api.HandshakeExchange; @@ -15,8 +17,26 @@ public class HandshakeExchangeImpl extends HandshakeExchange { @Override public Object handle(HttpExchange exchange, Request body) throws IOException, BeaconClientException, BeaconServerException { + if (!checkAuth(body.getAuth())) { + throw new BeaconClientException("Authentication failed"); + } + var session = new BeaconSession(body.getClient(), UUID.randomUUID().toString()); AppBeaconServer.get().addSession(session); return Response.builder().token(session.getToken()).build(); } + + private boolean checkAuth(BeaconAuthMethod authMethod) { + if (authMethod instanceof BeaconAuthMethod.Local local) { + var c = local.getAuthFileContent().trim(); + return AppBeaconServer.get().getLocalAuthSecret().equals(c); + } + + if (authMethod instanceof BeaconAuthMethod.ApiKey key) { + var c = key.getKey().trim(); + return AppPrefs.get().apiKey().get().equals(c); + } + + return false; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/QueryExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/QueryExchangeImpl.java new file mode 100644 index 000000000..2b67bd9f9 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/QueryExchangeImpl.java @@ -0,0 +1,60 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.QueryExchange; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class QueryExchangeImpl extends QueryExchange { + + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var catMatcher = Pattern.compile(toRegex(msg.getCategoryFilter())); + var conMatcher = Pattern.compile(toRegex(msg.getConnectionFilter())); + + List 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; + } + + found.add(storeEntry); + } + + var mapped = new ArrayList(); + for (DataStoreEntry e : found) { + var cat = DataStorage.get().getStorePath(DataStorage.get().getStoreCategoryIfPresent(e.getCategoryUuid()).orElseThrow()); + var obj = QueryExchange.QueryResponse.builder() + .uuid(e.getUuid()).category(cat).connection(DataStorage.get() + .getStorePath(e)).type(e.getProvider().getId()).build(); + mapped.add(obj); + } + return Response.builder().found(mapped).build(); + } + + private String toRegex(String pattern) { + return pattern.replaceAll("\\*","[^\\\\]*?"); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java new file mode 100644 index 000000000..ad72fed64 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java @@ -0,0 +1,35 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.ShellExecExchange; +import lombok.SneakyThrows; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +public class ShellExecExchangeImpl extends ShellExecExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var e = DataStorage.get().getStoreEntryIfPresent(msg.getConnection()).orElseThrow(() -> new IllegalArgumentException("Unknown connection")); + var existing = AppBeaconServer.get().getShellSessions().stream().filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)).findFirst(); + if (existing.isEmpty()) { + throw new BeaconClientException("No shell session active for connection"); + } + + AtomicReference out = new AtomicReference<>(); + AtomicReference err = new AtomicReference<>(); + long exitCode; + try (var command = existing.get().getControl().command(msg.getCommand()).start()) { + command.accumulateStdout(s -> out.set(s)); + command.accumulateStderr(s -> err.set(s)); + exitCode = command.getExitCode(); + } + return Response.builder().stdout(out.get()).stderr(err.get()).exitCode(exitCode).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java new file mode 100644 index 000000000..085b8e714 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java @@ -0,0 +1,34 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconShellSession; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.ShellStartExchange; +import io.xpipe.core.store.ShellStore; +import lombok.SneakyThrows; + +import java.io.IOException; + +public class ShellStartExchangeImpl extends ShellStartExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var e = DataStorage.get().getStoreEntryIfPresent(msg.getConnection()).orElseThrow(() -> new IllegalArgumentException("Unknown connection")); + if (!(e.getStore() instanceof ShellStore s)) { + throw new BeaconClientException("Not a shell connection"); + } + + var existing = AppBeaconServer.get().getShellSessions().stream().filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)).findFirst(); + if (existing.isPresent()) { + return Response.builder().build(); + } + + var control = s.control().start(); + AppBeaconServer.get().getShellSessions().add(new BeaconShellSession(e, control)); + return Response.builder().build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java new file mode 100644 index 000000000..07f07eb8d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java @@ -0,0 +1,26 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.ShellStopExchange; +import lombok.SneakyThrows; + +import java.io.IOException; + +public class ShellStopExchangeImpl extends ShellStopExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var e = DataStorage.get().getStoreEntryIfPresent(msg.getConnection()).orElseThrow(() -> new IllegalArgumentException("Unknown connection")); + var existing = AppBeaconServer.get().getShellSessions().stream().filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)).findFirst(); + if (existing.isPresent()) { + existing.get().getControl().close(); + AppBeaconServer.get().getShellSessions().remove(existing.get()); + } + return Response.builder().build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java index f6c07d05b..64414f537 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java @@ -23,7 +23,7 @@ public abstract class BrowserSessionTab { this.browserModel = browserModel; this.entry = entry; this.name = DataStorage.get().getStoreDisplayName(entry.get()); - this.tooltip = DataStorage.get().getId(entry.getEntry()).toString(); + this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString(); } public abstract Comp comp(); diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java index d28129628..25d28ce12 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -12,8 +12,8 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; import io.xpipe.beacon.BeaconClient; import io.xpipe.beacon.BeaconClientInformation; -import io.xpipe.beacon.api.FocusExchange; -import io.xpipe.beacon.api.OpenExchange; +import io.xpipe.beacon.api.DaemonFocusExchange; +import io.xpipe.beacon.api.DaemonOpenExchange; import io.xpipe.core.process.OsType; import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeInstallation; @@ -85,16 +85,16 @@ public class LauncherCommand implements Callable { var port = AppBeaconServer.get().getPort(); var client = BeaconClient.tryEstablishConnection(port, BeaconClientInformation.DaemonInformation.builder().build()); if (client.isPresent()) { - client.get().performRequest(FocusExchange.Request.builder().mode(getEffectiveMode()).build()); + client.get().performRequest(DaemonFocusExchange.Request.builder().mode(getEffectiveMode()).build()); if (!inputs.isEmpty()) { client.get().performRequest( - OpenExchange.Request.builder().arguments(inputs).build()); + DaemonOpenExchange.Request.builder().arguments(inputs).build()); } if (OsType.getLocal().equals(OsType.MACOS)) { Desktop.getDesktop().setOpenURIHandler(e -> { try { - client.get().performRequest(OpenExchange.Request.builder().arguments(List.of(e.getURI().toString())).build()); + client.get().performRequest(DaemonOpenExchange.Request.builder().arguments(List.of(e.getURI().toString())).build()); } catch (Exception ex) { ErrorEvent.fromThrowable(ex).expected().omit().handle(); } diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index 5e56d97cc..132bcdbce 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -122,11 +122,17 @@ public class AppPrefs { final Property httpServerPort = map(new SimpleObjectProperty<>(XPipeInstallation.getDefaultBeaconPort()), "httpServerPort", Integer.class); + final StringProperty apiKey = + map(new SimpleStringProperty(UUID.randomUUID().toString()), "apiKey", String.class); public ObservableValue httpServerPort() { return httpServerPort; } + public ObservableStringValue apiKey() { + return apiKey; + } + private final IntegerProperty editorReloadTimeout = map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class); private final BooleanProperty confirmDeletions = diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 62fa86aac..f1f50317f 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -5,10 +5,7 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.FixedHierarchyStore; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.DataStoreId; -import io.xpipe.core.store.FixedChildStore; -import io.xpipe.core.store.LocalStore; +import io.xpipe.core.store.*; import io.xpipe.core.util.UuidHelper; import javafx.util.Pair; import lombok.Getter; @@ -731,6 +728,22 @@ public abstract class DataStorage { return children; } + public List getCategoryParentHierarchy(DataStoreCategory cat) { + var es = new ArrayList(); + es.add(cat); + + DataStoreCategory current = cat; + while ((current = getStoreCategoryIfPresent(current.getParentCategory()).orElse(null)) != null) { + if (es.contains(current)) { + break; + } + + es.addFirst(current); + } + + return es; + } + public List getStoreParentHierarchy(DataStoreEntry entry) { var es = new ArrayList(); es.add(entry); @@ -747,34 +760,17 @@ public abstract class DataStorage { return es; } - public DataStoreId getId(DataStoreEntry entry) { - return DataStoreId.create(getStoreParentHierarchy(entry).stream() + public StorePath getStorePath(DataStoreEntry entry) { + return StorePath.create(getStoreParentHierarchy(entry).stream() .filter(e -> !(e.getStore() instanceof LocalStore)) - .map(e -> e.getName().replaceAll(":", "_")) + .map(e -> e.getName().replaceAll("/", "_")) .toArray(String[]::new)); } - public Optional getStoreEntryIfPresent(@NonNull DataStoreId id) { - var current = getStoreEntryIfPresent(id.getNames().getFirst()); - if (current.isPresent()) { - for (int i = 1; i < id.getNames().size(); i++) { - var children = getStoreChildren(current.get()); - int finalI = i; - current = children.stream() - .filter(dataStoreEntry -> dataStoreEntry - .getName() - .equalsIgnoreCase(id.getNames().get(finalI))) - .findFirst(); - if (current.isEmpty()) { - break; - } - } - - if (current.isPresent()) { - return current; - } - } - return Optional.empty(); + public StorePath getStorePath(DataStoreCategory entry) { + return StorePath.create(getCategoryParentHierarchy(entry).stream() + .map(e -> e.getName().replaceAll("/", "_")) + .toArray(String[]::new)); } public Optional getStoreEntryInProgressIfPresent(@NonNull DataStore store) { diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 36bd6ba7a..629815401 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -127,15 +127,9 @@ open module io.xpipe.app { AppLogs.Slf4jProvider; provides EventHandler with EventHandlerImpl; - provides BeaconInterface with - OpenExchangeImpl, - FocusExchangeImpl, - StatusExchangeImpl, - StopExchangeImpl, - HandshakeExchangeImpl, - ModeExchangeImpl, + provides BeaconInterface with ShellStartExchangeImpl, ShellStopExchangeImpl, ShellExecExchangeImpl, QueryExchangeImpl, DaemonOpenExchangeImpl, DaemonFocusExchangeImpl, DaemonStatusExchangeImpl, DaemonStopExchangeImpl, + HandshakeExchangeImpl, DaemonModeExchangeImpl, AskpassExchangeImpl, TerminalWaitExchangeImpl, - TerminalLaunchExchangeImpl, - VersionExchangeImpl; + TerminalLaunchExchangeImpl, DaemonVersionExchangeImpl; } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java b/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java new file mode 100644 index 000000000..2107a3260 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java @@ -0,0 +1,34 @@ +package io.xpipe.beacon; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type") +public interface BeaconAuthMethod { + + @JsonTypeName("local") + @Value + @Builder + @Jacksonized + public static class Local implements BeaconAuthMethod { + + @NonNull + String authFileContent; + } + + @JsonTypeName("apiKey") + @Value + @Builder + @Jacksonized + public static class ApiKey implements BeaconAuthMethod { + + @NonNull + String key; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index 1e7c635a8..40cddc25f 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -1,6 +1,6 @@ package io.xpipe.beacon; -import io.xpipe.beacon.api.StopExchange; +import io.xpipe.beacon.api.DaemonStopExchange; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileNames; import io.xpipe.core.util.XPipeDaemonMode; @@ -108,7 +108,7 @@ public class BeaconServer { } public static boolean tryStop(BeaconClient client) throws Exception { - StopExchange.Response res = client.performRequest(StopExchange.Request.builder().build()); + DaemonStopExchange.Response res = client.performRequest(DaemonStopExchange.Request.builder().build()); return res.isSuccess(); } diff --git a/beacon/src/main/java/io/xpipe/beacon/api/FocusExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java similarity index 80% rename from beacon/src/main/java/io/xpipe/beacon/api/FocusExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java index aac28bdbc..3a4bc0020 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/FocusExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java @@ -7,11 +7,11 @@ import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; -public class FocusExchange extends BeaconInterface { +public class DaemonFocusExchange extends BeaconInterface { @Override public String getPath() { - return "/focus"; + return "/daemon/focus"; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ModeExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java similarity index 82% rename from beacon/src/main/java/io/xpipe/beacon/api/ModeExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java index c11a98ee2..b9bcf2fd0 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ModeExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java @@ -7,11 +7,11 @@ import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; -public class ModeExchange extends BeaconInterface { +public class DaemonModeExchange extends BeaconInterface { @Override public String getPath() { - return "/mode"; + return "/daemon/mode"; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/api/OpenExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java similarity index 79% rename from beacon/src/main/java/io/xpipe/beacon/api/OpenExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java index 62172093c..1fe22076a 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/OpenExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java @@ -8,11 +8,11 @@ import lombok.extern.jackson.Jacksonized; import java.util.List; -public class OpenExchange extends BeaconInterface { +public class DaemonOpenExchange extends BeaconInterface { @Override public String getPath() { - return "/open"; + return "/daemon/open"; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/api/StatusExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java similarity index 76% rename from beacon/src/main/java/io/xpipe/beacon/api/StatusExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java index 0c4a2eade..379e7b0eb 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/StatusExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java @@ -5,11 +5,11 @@ import lombok.Builder; import lombok.Value; import lombok.extern.jackson.Jacksonized; -public class StatusExchange extends BeaconInterface { +public class DaemonStatusExchange extends BeaconInterface { @Override public String getPath() { - return "/status"; + return "/daemon/status"; } @Value diff --git a/beacon/src/main/java/io/xpipe/beacon/api/StopExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java similarity index 78% rename from beacon/src/main/java/io/xpipe/beacon/api/StopExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java index abfcf693f..5ae633f35 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/StopExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java @@ -8,11 +8,11 @@ import lombok.extern.jackson.Jacksonized; /** * Requests the daemon to stop. */ -public class StopExchange extends BeaconInterface { +public class DaemonStopExchange extends BeaconInterface { @Override public String getPath() { - return "/stop"; + return "/daemon/stop"; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/api/VersionExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java similarity index 78% rename from beacon/src/main/java/io/xpipe/beacon/api/VersionExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java index 42d4fa5e0..18c107ebd 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/VersionExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java @@ -5,11 +5,11 @@ import lombok.Builder; import lombok.Value; import lombok.extern.jackson.Jacksonized; -public class VersionExchange extends BeaconInterface { +public class DaemonVersionExchange extends BeaconInterface { @Override public String getPath() { - return "/version"; + return "/daemon/version"; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java index 2379db5d6..efaeba11c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java @@ -1,8 +1,10 @@ package io.xpipe.beacon.api; +import io.xpipe.beacon.BeaconAuthMethod; import io.xpipe.beacon.BeaconClientInformation; import io.xpipe.beacon.BeaconInterface; import lombok.Builder; +import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; @@ -22,6 +24,9 @@ public class HandshakeExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/query"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String categoryFilter; + @NonNull + String connectionFilter; + @NonNull + String typeFilter; + } + + @Jacksonized + @Builder + @Value + public static class Response { + List found; + } + + @Jacksonized + @Builder + @Value + public static class QueryResponse { + UUID uuid; + StorePath category; + StorePath connection; + String type; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java new file mode 100644 index 000000000..3ad47bcb3 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java @@ -0,0 +1,38 @@ +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.UUID; + +public class ShellExecExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/exec"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + @NonNull + String command; + } + + @Jacksonized + @Builder + @Value + public static class Response { + long exitCode; + @NonNull + String stdout; + @NonNull + String stderr; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java new file mode 100644 index 000000000..0bb995cc9 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java @@ -0,0 +1,30 @@ +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.UUID; + +public class ShellStartExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/start"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java new file mode 100644 index 000000000..3c2532658 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java @@ -0,0 +1,30 @@ +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.UUID; + +public class ShellStopExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/stop"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index cc55ad4e2..873aa3e27 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -24,9 +24,9 @@ open module io.xpipe.beacon { BeaconInterface.Loader; provides Module with BeaconJacksonModule; - provides BeaconInterface with ModeExchange,StatusExchange, FocusExchange, OpenExchange, StopExchange, HandshakeExchange, + provides BeaconInterface with ShellStartExchange, ShellStopExchange, ShellExecExchange, DaemonModeExchange, DaemonStatusExchange, DaemonFocusExchange, DaemonOpenExchange, DaemonStopExchange, HandshakeExchange, + QueryExchange, AskpassExchange, TerminalWaitExchange, - TerminalLaunchExchange, - VersionExchange; + TerminalLaunchExchange, DaemonVersionExchange; } diff --git a/core/src/main/java/io/xpipe/core/store/DataStoreId.java b/core/src/main/java/io/xpipe/core/store/DataStoreId.java index 6a60bd94a..4c24f0b6a 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStoreId.java +++ b/core/src/main/java/io/xpipe/core/store/DataStoreId.java @@ -1,6 +1,5 @@ package io.xpipe.core.store; -import com.fasterxml.jackson.annotation.JsonCreator; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -28,7 +27,6 @@ public class DataStoreId { private final List names; - @JsonCreator public DataStoreId(List names) { this.names = names; } diff --git a/core/src/main/java/io/xpipe/core/store/StorePath.java b/core/src/main/java/io/xpipe/core/store/StorePath.java new file mode 100644 index 000000000..4d3b47ba1 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/StorePath.java @@ -0,0 +1,84 @@ +package io.xpipe.core.store; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents a reference to an XPipe storage location. + *

+ * To allow for a simple usage, the names are trimmed and + * converted to lower case names when creating them. + * The names are separated by a slash and are therefore not allowed to contain slashes themselves. + * + * @see #fromString(String) + */ +@EqualsAndHashCode +@Getter +public class StorePath { + + public static final char SEPARATOR = '/'; + + private final List names; + + @JsonCreator + public StorePath(List names) { + this.names = names; + } + + /** + * Creates a new store path. + * + * @throws IllegalArgumentException if any name is not valid + */ + public static StorePath create(String... names) { + if (names == null) { + throw new IllegalArgumentException("Names are null"); + } + + if (Arrays.stream(names).anyMatch(s -> s == null)) { + throw new IllegalArgumentException("Name is null"); + } + + if (Arrays.stream(names).anyMatch(s -> s.contains("" + SEPARATOR))) { + throw new IllegalArgumentException("Separator character " + SEPARATOR + " is not allowed in the names"); + } + + if (Arrays.stream(names).anyMatch(s -> s.trim().length() == 0)) { + throw new IllegalArgumentException("Trimmed entry name is empty"); + } + + return new StorePath(Arrays.stream(names).toList()); + } + + /** + * Creates a new store path from a string representation. + * + * @param s the string representation, must be not null and fulfill certain requirements + * @throws IllegalArgumentException if the string is not valid + */ + public static StorePath fromString(String s) { + if (s == null) { + throw new IllegalArgumentException("String is null"); + } + + var split = s.split(String.valueOf(SEPARATOR), -1); + + var names = + Arrays.stream(split).map(String::trim).map(String::toLowerCase).toList(); + if (names.stream().anyMatch(s1 -> s1.isEmpty())) { + throw new IllegalArgumentException("Name must not be empty"); + } + + return new StorePath(names); + } + + @Override + public String toString() { + return names.stream().map(String::toLowerCase).collect(Collectors.joining("" + SEPARATOR)); + } +} diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java index e827bd269..72e7f5add 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -30,6 +30,10 @@ public class XPipeInstallation { return isStaging() ? "io.xpipe.xpipe-ptb" : "io.xpipe.xpipe"; } + public static Path getLocalBeaconAuthFile() { + return Path.of(System.getProperty("java.io.tmpdir"), "xpipe_auth"); + } + public static String createExternalAsyncLaunchCommand( String installationBase, XPipeDaemonMode mode, String arguments, boolean restart) { var suffix = (arguments != null ? " " + arguments : "");