api rework [stage]

This commit is contained in:
crschnick 2024-06-17 17:51:36 +00:00
parent 33577ca7c1
commit c5608bd23c
23 changed files with 1010 additions and 136 deletions

View file

@ -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<BeaconShellSession> shellSessions = new HashSet<>();
Map<UUID, byte[]> 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;
}
}

View file

@ -35,7 +35,7 @@ public class AppBeaconServer {
private final Set<BeaconSession> sessions = new HashSet<>();
@Getter
private final Set<BeaconShellSession> shellSessions = new HashSet<>();
private final AppBeaconCache cache = new AppBeaconCache();
@Getter
private String localAuthSecret;

View file

@ -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<T> implements HttpHandler {
@ -63,14 +63,19 @@ public class BeaconRequestHandler<T> 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<T> 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<T> 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<T> implements HttpHandler {
m.setAccessible(true);
return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b));
}
@SneakyThrows
@SuppressWarnings("unchecked")
private <REQ> 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));
}
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.beacon;
import io.xpipe.beacon.BeaconClientInformation;
import lombok.Value;
@Value

View file

@ -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;
}

View file

@ -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: "

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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<String> out = new AtomicReference<>();
AtomicReference<String> 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();

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -138,7 +138,9 @@ open module io.xpipe.app {
DaemonStatusExchangeImpl,
DaemonStopExchangeImpl,
HandshakeExchangeImpl,
DaemonModeExchangeImpl,
DaemonModeExchangeImpl, FsBlobExchangeImpl,
FsScriptExchangeImpl,
FsWriteExchangeImpl,
AskpassExchangeImpl,
TerminalWaitExchangeImpl,
TerminalLaunchExchangeImpl,

View file

@ -26,7 +26,7 @@ headingLevel: 2
The XPipe API provides programmatic access to XPipes 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:
<a href="/openapi.yaml" style="font-size: 20px">OpenAPI .yaml specification</a>
<a download href="/openapi.yaml" style="font-size: 20px">OpenAPI .yaml specification</a>
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 \
</details>
## Store a raw blob to be used later
<a id="opIdfsData"></a>
`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
```
<h3 id="store-a-raw-blob-to-be-used-later-parameters">Parameters</h3>
|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"
}
```
<h3 id="store-a-raw-blob-to-be-used-later-responses">Responses</h3>
|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|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
bearerAuth
</aside>
<details>
<summary>Code samples</summary>
```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
'
```
</details>
## Write a blob to a remote file
<a id="opIdfsWrite"></a>
`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"
}
```
<h3 id="write-a-blob-to-a-remote-file-parameters">Parameters</h3>
|Name|In|Type|Required|Description|
|---|---|---|---|---|
|body|body|[FsWriteRequest](#schemafswriterequest)|true|none|
<h3 id="write-a-blob-to-a-remote-file-responses">Responses</h3>
|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|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
bearerAuth
</aside>
<details>
<summary>Code samples</summary>
```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"
}
'
```
</details>
## Create a shell script file from a blob
<a id="opIdfsScript"></a>
`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"
}
```
<h3 id="create-a-shell-script-file-from-a-blob-parameters">Parameters</h3>
|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"
}
```
<h3 id="create-a-shell-script-file-from-a-blob-responses">Responses</h3>
|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|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
bearerAuth
</aside>
<details>
<summary>Code samples</summary>
```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"
}
'
```
</details>
# Schemas
<h2 id="tocS_ShellStartRequest">ShellStartRequest</h2>
@ -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|
<h2 id="tocS_FsBlobResponse">FsBlobResponse</h2>
<a id="schemafsblobresponse"></a>
<a id="schema_FsBlobResponse"></a>
<a id="tocSfsblobresponse"></a>
<a id="tocsfsblobresponse"></a>
```json
{
"blob": "string"
}
```
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|blob|string|true|none|The data uuid|
<h2 id="tocS_FsWriteRequest">FsWriteRequest</h2>
<a id="schemafswriterequest"></a>
<a id="schema_FsWriteRequest"></a>
<a id="tocSfswriterequest"></a>
<a id="tocsfswriterequest"></a>
```json
{
"connection": "string",
"blob": "string",
"path": "string"
}
```
<h3>Properties</h3>
|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|
<h2 id="tocS_FsScriptRequest">FsScriptRequest</h2>
<a id="schemafsscriptrequest"></a>
<a id="schema_FsScriptRequest"></a>
<a id="tocSfsscriptrequest"></a>
<a id="tocsfsscriptrequest"></a>
```json
{
"connection": "string",
"blob": "string"
}
```
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|connection|string|true|none|The connection uuid|
|blob|string|true|none|The blob uuid|
<h2 id="tocS_FsScriptResponse">FsScriptResponse</h2>
<a id="schemafsscriptresponse"></a>
<a id="schema_FsScriptResponse"></a>
<a id="tocSfsscriptresponse"></a>
<a id="tocsfsscriptresponse"></a>
```json
{
"path": "string"
}
```
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|path|string|true|none|The generated script file path|
<h2 id="tocS_ConnectionQueryRequest">ConnectionQueryRequest</h2>
<a id="schemaconnectionqueryrequest"></a>
@ -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|
<h2 id="tocS_HandshakeRequest">HandshakeRequest</h2>
@ -1117,10 +1671,6 @@ curl -X POST http://localhost:21723/shell/exec \
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|type|string|true|none|none|
oneOf
|Name|Type|Required|Restrictions|Description|
@ -1152,18 +1702,10 @@ API key authentication
<h3>Properties</h3>
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|
<h2 id="tocS_Local">Local</h2>
@ -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
<h3>Properties</h3>
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.|
<h2 id="tocS_ClientInformation">ClientInformation</h2>

View file

@ -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<FsBlobExchange.Request> {
@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;
}
}

View file

@ -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<FsScriptExchange.Request> {
@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;
}
}

View file

@ -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<FsWriteExchange.Request> {
@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 {}
}

View file

@ -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<ShellControl> getShell() {
return Optional.of(shellControl);

View file

@ -20,8 +20,6 @@ public interface FileSystem extends Closeable, AutoCloseable {
long getFileSize(String file) throws Exception;
FileSystemStore getStore();
Optional<ShellControl> getShell();
FileSystem open() throws Exception;

View file

@ -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() {

View file

@ -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<FilePath> {
@Override
public void serialize(FilePath value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.toString());
}
}
public static class FilePathDeserializer extends JsonDeserializer<FilePath> {
@Override
public FilePath deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return new FilePath(p.getValueAsString());
}
}
public static class CharsetSerializer extends JsonSerializer<Charset> {
@Override

View file

@ -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:

View file

@ -1 +1 @@
10.0-7
10.0-9