From c5608bd23cb7e720289bc7a410d335cb0f26e734 Mon Sep 17 00:00:00 2001 From: crschnick Date: Mon, 17 Jun 2024 17:51:36 +0000 Subject: [PATCH] api rework [stage] --- .../io/xpipe/app/beacon/AppBeaconCache.java | 33 + .../io/xpipe/app/beacon/AppBeaconServer.java | 2 +- .../app/beacon/BeaconRequestHandler.java | 47 +- .../io/xpipe/app/beacon/BeaconSession.java | 1 - .../impl/ConnectionQueryExchangeImpl.java | 2 +- .../beacon/impl/DaemonModeExchangeImpl.java | 16 +- .../app/beacon/impl/FsBlobExchangeImpl.java | 19 + .../app/beacon/impl/FsScriptExchangeImpl.java | 22 + .../app/beacon/impl/FsWriteExchangeImpl.java | 22 + .../beacon/impl/ShellExecExchangeImpl.java | 22 +- .../beacon/impl/ShellStartExchangeImpl.java | 10 +- .../beacon/impl/ShellStopExchangeImpl.java | 23 +- app/src/main/java/module-info.java | 4 +- .../io/xpipe/app/resources/misc/api.md | 631 ++++++++++++++++-- .../io/xpipe/beacon/api/FsBlobExchange.java | 32 + .../io/xpipe/beacon/api/FsScriptExchange.java | 36 + .../io/xpipe/beacon/api/FsWriteExchange.java | 35 + .../core/store/ConnectionFileSystem.java | 11 +- .../java/io/xpipe/core/store/FileSystem.java | 2 - .../java/io/xpipe/core/store/ShellStore.java | 2 +- .../io/xpipe/core/util/CoreJacksonModule.java | 23 + openapi.yaml | 149 +++++ version | 2 +- 23 files changed, 1010 insertions(+), 136 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java create mode 100644 app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java create mode 100644 app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java create mode 100644 app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java create mode 100644 beacon/src/main/java/io/xpipe/beacon/api/FsBlobExchange.java create mode 100644 beacon/src/main/java/io/xpipe/beacon/api/FsScriptExchange.java create mode 100644 beacon/src/main/java/io/xpipe/beacon/api/FsWriteExchange.java diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java new file mode 100644 index 000000000..2663a313f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java @@ -0,0 +1,33 @@ +package io.xpipe.app.beacon; + +import io.xpipe.beacon.BeaconClientException; +import lombok.Value; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Value +public class AppBeaconCache { + + Set shellSessions = new HashSet<>(); + Map savedBlobs = new ConcurrentHashMap<>(); + + public BeaconShellSession getShellSession(UUID uuid) throws BeaconClientException { + var found = shellSessions.stream().filter(beaconShellSession -> beaconShellSession.getEntry().getUuid().equals(uuid)).findFirst(); + if (found.isEmpty()) { + throw new BeaconClientException("No active shell session known for id " + uuid); + } + return found.get(); + } + + public byte[] getBlob(UUID uuid) throws BeaconClientException { + var found = savedBlobs.get(uuid); + if (found == null) { + throw new BeaconClientException("No saved data known for id " + uuid); + } + return found; + } +} 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 ecf7aedc8..fe24d5a35 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -35,7 +35,7 @@ public class AppBeaconServer { private final Set sessions = new HashSet<>(); @Getter - private final Set shellSessions = new HashSet<>(); + private final AppBeaconCache cache = new AppBeaconCache(); @Getter private String localAuthSecret; diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java index ae244a46c..32abfc794 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -1,5 +1,7 @@ package io.xpipe.app.beacon; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; @@ -7,15 +9,13 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; import io.xpipe.beacon.*; import io.xpipe.core.util.JacksonMapper; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; import lombok.SneakyThrows; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; public class BeaconRequestHandler implements HttpHandler { @@ -63,14 +63,19 @@ public class BeaconRequestHandler implements HttpHandler { Object response; try { try (InputStream is = exchange.getRequestBody()) { - var tree = JacksonMapper.getDefault().readTree(is); - TrackEvent.trace("Parsed raw request:\n" + tree.toPrettyString()); - var emptyRequestClass = - tree.isEmpty() && beaconInterface.getRequestClass().getDeclaredFields().length == 0; - object = emptyRequestClass - ? createDefaultRequest(beaconInterface) - : JacksonMapper.getDefault().treeToValue(tree, beaconInterface.getRequestClass()); - TrackEvent.trace("Parsed request object:\n" + object); + var read = is.readAllBytes(); + var rawDataRequestClass = beaconInterface.getRequestClass().getDeclaredFields().length == 1 && + beaconInterface.getRequestClass().getDeclaredFields()[0].getType().equals(byte[].class); + if (!new String(read, StandardCharsets.US_ASCII).trim().startsWith("{") && rawDataRequestClass) { + object = createRawDataRequest(beaconInterface,read); + } else { + var tree = JacksonMapper.getDefault().readTree(read); + TrackEvent.trace("Parsed raw request:\n" + tree.toPrettyString()); + var emptyRequestClass = tree.isEmpty() && beaconInterface.getRequestClass().getDeclaredFields().length == 0; + object = emptyRequestClass ? createDefaultRequest(beaconInterface) : JacksonMapper.getDefault().treeToValue(tree, + beaconInterface.getRequestClass()); + TrackEvent.trace("Parsed request object:\n" + object); + } } response = beaconInterface.handle(exchange, object); } catch (BeaconClientException clientException) { @@ -79,7 +84,7 @@ public class BeaconRequestHandler implements HttpHandler { return; } catch (BeaconServerException serverException) { var cause = serverException.getCause() != null ? serverException.getCause() : serverException; - ErrorEvent.fromThrowable(cause).handle(); + ErrorEvent.fromThrowable(cause).omit().expected().handle(); writeError(exchange, new BeaconServerErrorResponse(cause), 500); return; } catch (IOException ex) { @@ -93,7 +98,7 @@ public class BeaconRequestHandler implements HttpHandler { } return; } catch (Throwable other) { - ErrorEvent.fromThrowable(other).handle(); + ErrorEvent.fromThrowable(other).omit().expected().handle(); writeError(exchange, new BeaconServerErrorResponse(other), 500); return; } @@ -143,4 +148,20 @@ public class BeaconRequestHandler implements HttpHandler { m.setAccessible(true); return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b)); } + + @SneakyThrows + @SuppressWarnings("unchecked") + private REQ createRawDataRequest(BeaconInterface beaconInterface, byte[] s) { + var c = beaconInterface.getRequestClass().getDeclaredMethod("builder"); + c.setAccessible(true); + + var b = c.invoke(null); + var setMethod = Arrays.stream(b.getClass().getDeclaredMethods()).filter(method -> method.getParameterCount() == 1 && + method.getParameters()[0].getType().equals(byte[].class)).findFirst().orElseThrow(); + setMethod.invoke(b, (Object) s); + + var m = b.getClass().getDeclaredMethod("build"); + m.setAccessible(true); + return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b)); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java b/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java index 69c549147..38cec0f23 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java @@ -1,7 +1,6 @@ package io.xpipe.app.beacon; import io.xpipe.beacon.BeaconClientInformation; - import lombok.Value; @Value diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java index 5ec23e36f..5d747beda 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java @@ -41,7 +41,7 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange { continue; } - if (!typeMatcher.matcher(storeEntry.getProvider().getId()).matches()) { + if (!typeMatcher.matcher(storeEntry.getProvider().getId().toLowerCase()).matches()) { continue; } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java index cf866f8b2..3e5779d16 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java @@ -1,24 +1,14 @@ package io.xpipe.app.beacon.impl; -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.DaemonModeExchange; - import com.sun.net.httpserver.HttpExchange; - -import java.io.IOException; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.api.DaemonModeExchange; public class DaemonModeExchangeImpl extends DaemonModeExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { - // Wait for startup - while (OperationMode.get() == null) { - ThreadHelper.sleep(100); - } - var mode = OperationMode.map(msg.getMode()); if (!mode.isSupported()) { throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName() + ". Supported: " diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java new file mode 100644 index 000000000..dca23b200 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java @@ -0,0 +1,19 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.beacon.api.FsBlobExchange; +import lombok.SneakyThrows; + +import java.util.UUID; + +public class FsBlobExchangeImpl extends FsBlobExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) { + var id = UUID.randomUUID(); + AppBeaconServer.get().getCache().getSavedBlobs().put(id, msg.getPayload()); + return Response.builder().blob(id).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java new file mode 100644 index 000000000..043f67e85 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java @@ -0,0 +1,22 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.util.ScriptHelper; +import io.xpipe.beacon.api.FsScriptExchange; +import lombok.SneakyThrows; + +import java.nio.charset.StandardCharsets; + +public class FsScriptExchangeImpl extends FsScriptExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) { + var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); + var data = new String(AppBeaconServer.get().getCache().getBlob(msg.getBlob()), StandardCharsets.UTF_8); + var file = ScriptHelper.getExecScriptFile(shell.getControl()); + shell.getControl().getShellDialect().createScriptTextFileWriteCommand(shell.getControl(), data, file.toString()); + return Response.builder().path(file).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java new file mode 100644 index 000000000..a91fa517f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java @@ -0,0 +1,22 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.beacon.api.FsWriteExchange; +import io.xpipe.core.store.ConnectionFileSystem; +import lombok.SneakyThrows; + +public class FsWriteExchangeImpl extends FsWriteExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) { + var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); + var data = AppBeaconServer.get().getCache().getBlob(msg.getBlob()); + var fs = new ConnectionFileSystem(shell.getControl()); + try (var os = fs.openOutput(msg.getPath().toString(), data.length)) { + os.write(data); + } + return Response.builder().build(); + } +} 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 index 218b5f764..6ff63bcae 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java @@ -1,15 +1,10 @@ package io.xpipe.app.beacon.impl; -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 com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.beacon.api.ShellExecExchange; import lombok.SneakyThrows; -import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; public class ShellExecExchangeImpl extends ShellExecExchange { @@ -17,20 +12,11 @@ public class ShellExecExchangeImpl extends ShellExecExchange { @Override @SneakyThrows public Object handle(HttpExchange exchange, Request msg) { - 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"); - } - + var existing = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); AtomicReference out = new AtomicReference<>(); AtomicReference err = new AtomicReference<>(); long exitCode; - try (var command = existing.get().getControl().command(msg.getCommand()).start()) { + try (var command = existing.getControl().command(msg.getCommand()).start()) { command.accumulateStdout(s -> out.set(s)); command.accumulateStderr(s -> err.set(s)); exitCode = command.getExitCode(); 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 index 611ec45e2..0471fb36e 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java @@ -1,18 +1,14 @@ 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 com.sun.net.httpserver.HttpExchange; import lombok.SneakyThrows; -import java.io.IOException; - public class ShellStartExchangeImpl extends ShellStartExchange { @Override @@ -25,7 +21,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange { throw new BeaconClientException("Not a shell connection"); } - var existing = AppBeaconServer.get().getShellSessions().stream() + var existing = AppBeaconServer.get().getCache().getShellSessions().stream() .filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)) .findFirst(); if (existing.isPresent()) { @@ -33,7 +29,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange { } var control = s.control().start(); - AppBeaconServer.get().getShellSessions().add(new BeaconShellSession(e, control)); + AppBeaconServer.get().getCache().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 index 35bef009a..ef349b72e 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java @@ -1,31 +1,18 @@ package io.xpipe.app.beacon.impl; -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 com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +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) { - 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()); - } + var e = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); + e.getControl().close(); + AppBeaconServer.get().getCache().getShellSessions().remove(e); return Response.builder().build(); } } diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 2966b7928..751995662 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -138,7 +138,9 @@ open module io.xpipe.app { DaemonStatusExchangeImpl, DaemonStopExchangeImpl, HandshakeExchangeImpl, - DaemonModeExchangeImpl, + DaemonModeExchangeImpl, FsBlobExchangeImpl, + FsScriptExchangeImpl, + FsWriteExchangeImpl, AskpassExchangeImpl, TerminalWaitExchangeImpl, TerminalLaunchExchangeImpl, diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/api.md b/app/src/main/resources/io/xpipe/app/resources/misc/api.md index ad98bbb12..d53af5952 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/api.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md @@ -26,7 +26,7 @@ headingLevel: 2 The XPipe API provides programmatic access to XPipe’s features. You can get started by either using this page as an API reference or alternatively import the OpenAPI definition file into your API client of choice: -OpenAPI .yaml specification +OpenAPI .yaml specification The XPipe application will start up an HTTP server that can be used to send requests. You can change the port of it in the settings menu. @@ -279,22 +279,22 @@ All matching is case insensitive. { "found": [ { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "category": [ "default" ], - "connection": [ + "name": [ "local machine" ], "type": "local" }, { - "uuid": "e1462ddc-9beb-484c-bd91-bb666027e300", + "connection": "e1462ddc-9beb-484c-bd91-bb666027e300", "category": [ "default", "category 1" ], - "connection": [ + "name": [ "ssh system", "shell environments", "bash" @@ -453,7 +453,7 @@ These errors will be returned with the HTTP return code 500. ```json { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } ``` @@ -485,7 +485,7 @@ bearerAuth ```javascript const inputBody = '{ - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" }'; const headers = { 'Content-Type':'application/json', @@ -515,7 +515,7 @@ headers = { data = """ { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } """ r = requests.post('http://localhost:21723/shell/start', headers = headers, data = data) @@ -534,7 +534,7 @@ var request = HttpRequest .header("Authorization", "Bearer {access-token}") .POST(HttpRequest.BodyPublishers.ofString(""" { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } """)) .build(); @@ -576,7 +576,7 @@ curl -X POST http://localhost:21723/shell/start \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \ --data ' { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } ' @@ -599,7 +599,7 @@ If the shell is busy or stuck, you might have to work with timeouts to account f ```json { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } ``` @@ -631,7 +631,7 @@ bearerAuth ```javascript const inputBody = '{ - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" }'; const headers = { 'Content-Type':'application/json', @@ -661,7 +661,7 @@ headers = { data = """ { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } """ r = requests.post('http://localhost:21723/shell/stop', headers = headers, data = data) @@ -680,7 +680,7 @@ var request = HttpRequest .header("Authorization", "Bearer {access-token}") .POST(HttpRequest.BodyPublishers.ofString(""" { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } """)) .build(); @@ -722,7 +722,7 @@ curl -X POST http://localhost:21723/shell/stop \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \ --data ' { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } ' @@ -746,7 +746,7 @@ However, if any other error occurs like the shell not responding or exiting unex ```json { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "echo $USER" } ``` @@ -799,7 +799,7 @@ bearerAuth ```javascript const inputBody = '{ - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "echo $USER" }'; const headers = { @@ -832,7 +832,7 @@ headers = { data = """ { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "echo $USER" } """ @@ -853,7 +853,7 @@ var request = HttpRequest .header("Authorization", "Bearer {access-token}") .POST(HttpRequest.BodyPublishers.ofString(""" { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "echo $USER" } """)) @@ -897,7 +897,7 @@ curl -X POST http://localhost:21723/shell/exec \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \ --data ' { - "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "echo $USER" } ' @@ -906,6 +906,474 @@ curl -X POST http://localhost:21723/shell/exec \ +## Store a raw blob to be used later + + + +`POST /fs/blob` + +Stores arbitrary binary data in a blob such that it can be used later on to for example write to a remote file. + +This will return a uuid which can be used as a reference to the blob. +You can also store normal text data in blobs if you intend to create text or shell script files with it. + +> Body parameter + +```yaml +string + +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|string(binary)|true|none| + +> Example responses + +> The operation was successful. The data was stored. + +```json +{ + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The data was stored.|[FsBlobResponse](#schemafsblobresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = 'string'; +const headers = { + 'Content-Type':'application/octet-stream', + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21723/fs/blob', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/octet-stream', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +string +""" +r = requests.post('http://localhost:21723/fs/blob', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21723/fs/blob"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/octet-stream") + .header("Accept", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +string + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/octet-stream"}, + "Accept": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21723/fs/blob", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21723/fs/blob \ + -H 'Content-Type: application/octet-stream' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +string +' + +``` + +
+ +## Write a blob to a remote file + + + +`POST /fs/write` + +Writes blob data to a file through an active shell session. + +> Body parameter + +```json +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304", + "path": "/home/user/myfile.txt" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[FsWriteRequest](#schemafswriterequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The file was written.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304", + "path": "/home/user/myfile.txt" +}'; +const headers = { + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21723/fs/write', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304", + "path": "/home/user/myfile.txt" +} +""" +r = requests.post('http://localhost:21723/fs/write', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21723/fs/write"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304", + "path": "/home/user/myfile.txt" +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21723/fs/write", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21723/fs/write \ + -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304", + "path": "/home/user/myfile.txt" +} +' + +``` + +
+ +## Create a shell script file from a blob + + + +`POST /fs/script` + +Creates a shell script in the temporary directory of the file system that is access through the shell connection. + +This can be used to run more complex commands on remote systems. + +> Body parameter + +```json +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[FsScriptRequest](#schemafsscriptrequest)|true|none| + +> Example responses + +> The operation was successful. The script file was created. + +```json +{ + "path": "/tmp/exec-123.sh" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The script file was created.|[FsScriptResponse](#schemafsscriptresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" +}'; +const headers = { + 'Content-Type':'application/json', + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21723/fs/script', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" +} +""" +r = requests.post('http://localhost:21723/fs/script', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21723/fs/script"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Accept": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21723/fs/script", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21723/fs/script \ + -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +{ + "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" +} +' + +``` + +
+ # Schemas

ShellStartRequest

@@ -994,6 +1462,92 @@ curl -X POST http://localhost:21723/shell/exec \ |stdout|string|true|none|The stdout output of the command| |stderr|string|true|none|The stderr output of the command| +

FsBlobResponse

+ + + + + + +```json +{ + "blob": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|blob|string|true|none|The data uuid| + +

FsWriteRequest

+ + + + + + +```json +{ + "connection": "string", + "blob": "string", + "path": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connection|string|true|none|The connection uuid| +|blob|string|true|none|The blob uuid| +|path|string|true|none|The target filepath| + +

FsScriptRequest

+ + + + + + +```json +{ + "connection": "string", + "blob": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connection|string|true|none|The connection uuid| +|blob|string|true|none|The blob uuid| + +

FsScriptResponse

+ + + + + + +```json +{ + "path": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|path|string|true|none|The generated script file path| +

ConnectionQueryRequest

@@ -1029,11 +1583,11 @@ curl -X POST http://localhost:21723/shell/exec \ { "found": [ { - "uuid": "string", + "connection": "string", "category": [ "string" ], - "connection": [ + "name": [ "string" ], "type": "string" @@ -1048,9 +1602,9 @@ curl -X POST http://localhost:21723/shell/exec \ |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |found|[object]|true|none|The found connections| -|» uuid|string|true|none|The unique id of the connection| +|» connection|string|true|none|The unique id of the connection| |» category|[string]|true|none|The full category path as an array| -|» connection|[string]|true|none|The full connection name path as an array| +|» name|[string]|true|none|The full connection name path as an array| |» type|string|true|none|The type identifier of the connection|

HandshakeRequest

@@ -1117,10 +1671,6 @@ curl -X POST http://localhost:21723/shell/exec \

Properties

-|Name|Type|Required|Restrictions|Description| -|---|---|---|---|---| -|type|string|true|none|none| - oneOf |Name|Type|Required|Restrictions|Description| @@ -1152,18 +1702,10 @@ API key authentication

Properties

-allOf - discriminator: AuthMethod.type - |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| -|*anonymous*|[AuthMethod](#schemaauthmethod)|false|none|none| - -and - -|Name|Type|Required|Restrictions|Description| -|---|---|---|---|---| -|*anonymous*|object|false|none|none| -|» key|string|true|none|The API key| +|type|string|true|none|none| +|key|string|true|none|The API key|

Local

@@ -1175,7 +1717,6 @@ and ```json { "type": "string", - "key": "string", "authFileContent": "string" } @@ -1185,18 +1726,10 @@ Authentication method for local applications. Uses file system access as proof o

Properties

-allOf - discriminator: AuthMethod.type - |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| -|*anonymous*|[AuthMethod](#schemaauthmethod)|false|none|none| - -and - -|Name|Type|Required|Restrictions|Description| -|---|---|---|---|---| -|*anonymous*|object|false|none|none| -|» authFileContent|string|true|none|The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.| +|type|string|true|none|none| +|authFileContent|string|true|none|The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.|

ClientInformation

diff --git a/beacon/src/main/java/io/xpipe/beacon/api/FsBlobExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/FsBlobExchange.java new file mode 100644 index 000000000..d306efa99 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/FsBlobExchange.java @@ -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.UUID; + +public class FsBlobExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/blob"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + byte @NonNull [] payload; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + UUID blob; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/FsScriptExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/FsScriptExchange.java new file mode 100644 index 000000000..3b585fbff --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/FsScriptExchange.java @@ -0,0 +1,36 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.store.FilePath; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class FsScriptExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/script"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + @NonNull + UUID blob; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + FilePath path; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/FsWriteExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/FsWriteExchange.java new file mode 100644 index 000000000..919ecc20d --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/FsWriteExchange.java @@ -0,0 +1,35 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.store.FilePath; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class FsWriteExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/write"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + @NonNull + UUID blob; + @NonNull + FilePath path; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java index d87ab0b7a..6073bd7be 100644 --- a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java @@ -18,12 +18,8 @@ public class ConnectionFileSystem implements FileSystem { @JsonIgnore protected final ShellControl shellControl; - @JsonIgnore - protected final ShellStore store; - - public ConnectionFileSystem(ShellControl shellControl, ShellStore store) { + public ConnectionFileSystem(ShellControl shellControl) { this.shellControl = shellControl; - this.store = store; } @Override @@ -32,11 +28,6 @@ public class ConnectionFileSystem implements FileSystem { shellControl.getShellDialect().queryFileSize(shellControl, file).readStdoutOrThrow()); } - @Override - public FileSystemStore getStore() { - return store; - } - @Override public Optional getShell() { return Optional.of(shellControl); diff --git a/core/src/main/java/io/xpipe/core/store/FileSystem.java b/core/src/main/java/io/xpipe/core/store/FileSystem.java index 1439615cc..eb46022b6 100644 --- a/core/src/main/java/io/xpipe/core/store/FileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/FileSystem.java @@ -20,8 +20,6 @@ public interface FileSystem extends Closeable, AutoCloseable { long getFileSize(String file) throws Exception; - FileSystemStore getStore(); - Optional getShell(); FileSystem open() throws Exception; diff --git a/core/src/main/java/io/xpipe/core/store/ShellStore.java b/core/src/main/java/io/xpipe/core/store/ShellStore.java index 3d249b9ed..549b09780 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/ShellStore.java @@ -11,7 +11,7 @@ public interface ShellStore extends DataStore, LaunchableStore, FileSystemStore, @Override default FileSystem createFileSystem() { - return new ConnectionFileSystem(control(), this); + return new ConnectionFileSystem(control()); } default ProcessControl prepareLaunchCommand() { diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java index bebcba052..7f35d0f7f 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -16,6 +16,7 @@ import io.xpipe.core.dialog.HeaderElement; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellDialect; import io.xpipe.core.process.ShellDialects; +import io.xpipe.core.store.FilePath; import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.StorePath; @@ -44,6 +45,12 @@ public class CoreJacksonModule extends SimpleModule { context.registerSubtypes(new NamedType(t.getClass())); } + addSerializer(FilePath.class, new FilePathSerializer()); + addDeserializer(FilePath.class, new FilePathDeserializer()); + + addSerializer(StorePath.class, new StorePathSerializer()); + addDeserializer(StorePath.class, new StorePathDeserializer()); + addSerializer(Charset.class, new CharsetSerializer()); addDeserializer(Charset.class, new CharsetDeserializer()); @@ -88,6 +95,22 @@ public class CoreJacksonModule extends SimpleModule { } } + public static class FilePathSerializer extends JsonSerializer { + + @Override + public void serialize(FilePath value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(value.toString()); + } + } + + public static class FilePathDeserializer extends JsonDeserializer { + + @Override + public FilePath deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return new FilePath(p.getValueAsString()); + } + } + public static class CharsetSerializer extends JsonSerializer { @Override diff --git a/openapi.yaml b/openapi.yaml index 97d092fd6..23444d1f5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -228,6 +228,111 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' + /fs/blob: + post: + summary: Store a raw blob to be used later + description: | + Stores arbitrary binary data in a blob such that it can be used later on to for example write to a remote file. + + This will return a uuid which can be used as a reference to the blob. + You can also store normal text data in blobs if you intend to create text or shell script files with it. + operationId: fsData + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: The operation was successful. The data was stored. + content: + application/json: + schema: + $ref: '#/components/schemas/FsBlobResponse' + examples: + success: + summary: Success + value: { "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" } + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + /fs/write: + post: + summary: Write a blob to a remote file + description: | + Writes blob data to a file through an active shell session. + operationId: fsWrite + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FsWriteRequest' + examples: + simple: + summary: Write simple file + value: { "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "blob": "854afc45-eadc-49a0-a45d-9fb76a484304", "path": "/home/user/myfile.txt" } + responses: + '200': + description: The operation was successful. The file was written. + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + /fs/script: + post: + summary: Create a shell script file from a blob + description: | + Creates a shell script in the temporary directory of the file system that is access through the shell connection. + + This can be used to run more complex commands on remote systems. + operationId: fsScript + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FsScriptRequest' + examples: + standard: + summary: Standard write + value: { "connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "blob": "854afc45-eadc-49a0-a45d-9fb76a484304" } + responses: + '200': + description: The operation was successful. The script file was created. + content: + application/json: + schema: + $ref: '#/components/schemas/FsScriptResponse' + examples: + success: + summary: Success + value: { "path": "/tmp/exec-123.sh" } + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' components: schemas: ShellStartRequest: @@ -274,6 +379,50 @@ components: - exitCode - stdout - stderr + FsBlobResponse: + type: object + properties: + blob: + type: string + description: The data uuid + required: + - blob + FsWriteRequest: + type: object + properties: + connection: + type: string + description: The connection uuid + blob: + type: string + description: The blob uuid + path: + type: string + description: The target filepath + required: + - connection + - blob + - path + FsScriptRequest: + type: object + properties: + connection: + type: string + description: The connection uuid + blob: + type: string + description: The blob uuid + required: + - connection + - blob + FsScriptResponse: + type: object + properties: + path: + type: string + description: The generated script file path + required: + - path ConnectionQueryRequest: type: object properties: diff --git a/version b/version index 311288448..e9f695b24 100644 --- a/version +++ b/version @@ -1 +1 @@ -10.0-7 +10.0-9