Add interactive check and rework api [stage]

This commit is contained in:
crschnick 2024-06-18 00:20:45 +00:00
parent c5608bd23c
commit adb621dab4
16 changed files with 304 additions and 60 deletions

View file

@ -3,6 +3,8 @@ package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.util.AskpassAlert;
import io.xpipe.app.util.SecretManager;
import io.xpipe.app.util.SecretQueryState;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.AskpassExchange;
public class AskpassExchangeImpl extends AskpassExchange {
@ -13,7 +15,7 @@ public class AskpassExchangeImpl extends AskpassExchange {
}
@Override
public Object handle(HttpExchange exchange, Request msg) {
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
if (msg.getRequest() == null) {
var r = AskpassAlert.queryRaw(msg.getPrompt(), null);
return Response.builder().value(r.getSecret()).build();
@ -23,13 +25,16 @@ public class AskpassExchangeImpl extends AskpassExchange {
? SecretManager.getProgress(msg.getRequest(), msg.getSecretId())
: SecretManager.getProgress(msg.getRequest());
if (found.isEmpty()) {
return Response.builder().build();
throw new BeaconClientException("No password was provided");
}
var p = found.get();
var secret = p.process(msg.getPrompt());
if (p.getState() != SecretQueryState.NORMAL) {
throw new BeaconClientException(SecretQueryState.toErrorMessage(p.getState()));
}
return Response.builder()
.value(secret != null ? secret.inPlace() : null)
.value(secret.inPlace())
.build();
}
}

View file

@ -16,7 +16,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange {
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());
shell.getControl().getShellDialect().createScriptTextFileWriteCommand(shell.getControl(), data, file.toString()).execute();
return Response.builder().path(file).build();
}
}

View file

@ -24,12 +24,12 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
var existing = AppBeaconServer.get().getCache().getShellSessions().stream()
.filter(beaconShellSession -> beaconShellSession.getEntry().equals(e))
.findFirst();
if (existing.isPresent()) {
return Response.builder().build();
var control = (existing.isPresent() ? existing.get().getControl() : s.control());
control.setNonInteractive();
control.start();
if (existing.isEmpty()) {
AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(e, control));
}
var control = s.control().start();
AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(e, control));
return Response.builder().build();
return Response.builder().shellDialect(control.getShellDialect()).osType(control.getOsType()).osName(control.getOsName()).temp(control.getSystemTemporaryDirectory()).build();
}
}

View file

@ -21,7 +21,7 @@ public class AskpassAlert {
public static SecretQueryResult queryRaw(String prompt, InPlaceSecretValue secretValue) {
if (!PlatformState.initPlatformIfNeeded()) {
return new SecretQueryResult(null, true);
return new SecretQueryResult(null, SecretQueryState.CANCELLED);
}
AppStyle.init();
@ -103,6 +103,6 @@ public class AskpassAlert {
return prop.getValue() != null ? prop.getValue() : InPlaceSecretValue.of("");
})
.orElse(null);
return new SecretQueryResult(r, r == null);
return new SecretQueryResult(r, r == null ? SecretQueryState.CANCELLED : SecretQueryState.NORMAL);
}
}

View file

@ -34,8 +34,9 @@ public class SecretManager {
List<SecretQuery> suppliers,
SecretQuery fallback,
List<SecretQueryFilter> filters,
CountDown countDown) {
var p = new SecretQueryProgress(request, storeId, suppliers, fallback, filters, countDown);
CountDown countDown,
boolean interactive) {
var p = new SecretQueryProgress(request, storeId, suppliers, fallback, filters, countDown, interactive);
progress.add(p);
return p;
}
@ -55,14 +56,14 @@ public class SecretManager {
return false;
}
public static SecretValue retrieve(SecretRetrievalStrategy strategy, String prompt, UUID secretId, int sub) {
public static SecretValue retrieve(SecretRetrievalStrategy strategy, String prompt, UUID secretId, int sub, boolean interactive) {
if (!strategy.expectsQuery()) {
return null;
}
var uuid = UUID.randomUUID();
var p = expectAskpass(
uuid, secretId, List.of(strategy.query()), SecretQuery.prompt(false), List.of(), CountDown.of());
uuid, secretId, List.of(strategy.query()), SecretQuery.prompt(false), List.of(), CountDown.of(), interactive);
p.preAdvance(sub);
var r = p.process(prompt);
completeRequest(uuid);

View file

@ -32,13 +32,13 @@ public interface SecretQuery {
var inPlace = found.get().getSecret().inPlace();
var r = AskpassAlert.queryRaw(prompt, inPlace);
return r.isCancelled() ? Optional.of(r) : found;
return r.getState() != SecretQueryState.NORMAL ? Optional.of(r) : found;
}
@Override
public SecretQueryResult query(String prompt) {
var r = original.query(prompt);
if (r.isCancelled()) {
if (r.getState() != SecretQueryState.NORMAL) {
return r;
}
@ -96,7 +96,7 @@ public interface SecretQuery {
default Optional<SecretQueryResult> retrieveCache(String prompt, SecretReference reference) {
var r = SecretManager.get(reference);
return r.map(secretValue -> new SecretQueryResult(secretValue, false));
return r.map(secretValue -> new SecretQueryResult(secretValue, SecretQueryState.NORMAL));
}
SecretQueryResult query(String prompt);

View file

@ -22,7 +22,8 @@ public class SecretQueryProgress {
private final List<SecretQueryFilter> filters;
private final List<String> seenPrompts;
private final CountDown countDown;
private boolean requestCancelled;
private final boolean interactive;
private SecretQueryState state = SecretQueryState.NORMAL;
public SecretQueryProgress(
@NonNull UUID requestId,
@ -30,13 +31,16 @@ public class SecretQueryProgress {
@NonNull List<SecretQuery> suppliers,
@NonNull SecretQuery fallback,
@NonNull List<SecretQueryFilter> filters,
@NonNull CountDown countDown) {
@NonNull CountDown countDown,
boolean interactive
) {
this.requestId = requestId;
this.storeId = storeId;
this.suppliers = new ArrayList<>(suppliers);
this.fallback = fallback;
this.filters = filters;
this.countDown = countDown;
this.interactive = interactive;
this.seenPrompts = new ArrayList<>();
}
@ -49,7 +53,7 @@ public class SecretQueryProgress {
public SecretValue process(String prompt) {
// Cancel early
if (requestCancelled) {
if (state != SecretQueryState.NORMAL) {
return null;
}
@ -67,11 +71,18 @@ public class SecretQueryProgress {
var firstSeenIndex = seenPrompts.indexOf(prompt);
if (firstSeenIndex >= suppliers.size()) {
// Check whether we can have user inputs
if (!interactive && fallback.requiresUserInteraction()) {
state = SecretQueryState.NON_INTERACTIVE;
return null;
}
countDown.pause();
var r = fallback.query(prompt);
countDown.resume();
if (r.isCancelled()) {
requestCancelled = true;
if (r.getState() != SecretQueryState.NORMAL) {
state = r.getState();
return null;
}
return r.getSecret();
@ -82,6 +93,12 @@ public class SecretQueryProgress {
var shouldCache = shouldCache(sup, prompt);
var wasLastPrompt = firstSeenIndex == seenPrompts.size() - 1;
// Check whether we can have user inputs
if (!interactive && sup.requiresUserInteraction()) {
state = SecretQueryState.NON_INTERACTIVE;
return null;
}
// Clear cache if secret was wrong/queried again
// Check whether this is actually the last prompt seen as it might happen that
// previous prompts get rolled back again when one further down is wrong
@ -91,7 +108,7 @@ public class SecretQueryProgress {
// If we supplied a wrong secret and cannot retry, cancel the entire request
if (seenBefore && wasLastPrompt && !sup.retryOnFail()) {
requestCancelled = true;
state = SecretQueryState.FIXED_SECRET_WRONG;
return null;
}
@ -100,8 +117,8 @@ public class SecretQueryProgress {
var cached = sup.retrieveCache(prompt, ref);
countDown.resume();
if (cached.isPresent()) {
if (cached.get().isCancelled()) {
requestCancelled = true;
if (cached.get().getState() != SecretQueryState.NORMAL) {
state = cached.get().getState();
return null;
}
@ -113,8 +130,8 @@ public class SecretQueryProgress {
var r = sup.query(prompt);
countDown.resume();
if (r.isCancelled()) {
requestCancelled = true;
if (r.getState() != SecretQueryState.NORMAL) {
state = r.getState();
return null;
}

View file

@ -8,5 +8,5 @@ import lombok.Value;
public class SecretQueryResult {
SecretValue secret;
boolean cancelled;
SecretQueryState state;
}

View file

@ -0,0 +1,33 @@
package io.xpipe.app.util;
public enum SecretQueryState {
NORMAL,
CANCELLED,
NON_INTERACTIVE,
FIXED_SECRET_WRONG,
RETRIEVAL_FAILURE;
public static String toErrorMessage(SecretQueryState s) {
if (s == null) {
return "None";
}
return switch (s) {
case NORMAL -> {
yield "None";
}
case CANCELLED -> {
yield "Operation was cancelled";
}
case NON_INTERACTIVE -> {
yield "Session is not interactive but required user input";
}
case FIXED_SECRET_WRONG -> {
yield "Provided secret is wrong";
}
case RETRIEVAL_FAILURE -> {
yield "Failed to retrieve secret";
}
};
}
}

View file

@ -60,7 +60,7 @@ public interface SecretRetrievalStrategy {
@Override
public SecretQueryResult query(String prompt) {
return new SecretQueryResult(
value != null ? value.getInternalSecret() : InPlaceSecretValue.of(""), false);
value != null ? value.getInternalSecret() : InPlaceSecretValue.of(""), SecretQueryState.NORMAL);
}
@Override
@ -125,7 +125,7 @@ public interface SecretRetrievalStrategy {
public SecretQueryResult query(String prompt) {
var cmd = AppPrefs.get().passwordManagerString(key);
if (cmd == null) {
return new SecretQueryResult(null, true);
return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);
}
String r;
@ -134,7 +134,7 @@ public interface SecretRetrievalStrategy {
} catch (Exception ex) {
ErrorEvent.fromThrowable("Unable to retrieve password with command " + cmd, ex)
.handle();
return new SecretQueryResult(null, true);
return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);
}
if (r.lines().count() > 1 || r.isBlank()) {
@ -145,7 +145,7 @@ public interface SecretRetrievalStrategy {
+ " you will have to change the command and/or password key."));
}
return new SecretQueryResult(InPlaceSecretValue.of(r), false);
return new SecretQueryResult(InPlaceSecretValue.of(r), SecretQueryState.NORMAL);
}
@Override
@ -180,11 +180,11 @@ public interface SecretRetrievalStrategy {
@Override
public SecretQueryResult query(String prompt) {
try (var cc = new LocalStore().control().command(command).start()) {
return new SecretQueryResult(InPlaceSecretValue.of(cc.readStdoutOrThrow()), false);
return new SecretQueryResult(InPlaceSecretValue.of(cc.readStdoutOrThrow()), SecretQueryState.NORMAL);
} catch (Exception ex) {
ErrorEvent.fromThrowable("Unable to retrieve password with command " + command, ex)
.handle();
return new SecretQueryResult(null, true);
return new SecretQueryResult(null, SecretQueryState.RETRIEVAL_FAILURE);
}
}

View file

@ -29,7 +29,6 @@ You can get started by either using this page as an API reference or alternative
<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.
Note that this server is HTTP-only for now as it runs only on localhost. HTTPS requests are not accepted.
This allows you to programmatically manage remote systems.
@ -104,8 +103,8 @@ Note that for development you can also turn off the required authentication in t
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The handshake was successful. The returned token can be used for authentication in this session. The token is valid as long as XPipe is running.|[HandshakeResponse](#schemahandshakeresponse)|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="success">
This operation does not require authentication
@ -305,16 +304,24 @@ All matching is case insensitive.
}
```
> 400 Response
```json
{
"message": "string"
}
```
<h3 id="query-connections-responses">Responses</h3>
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The query was successful. The body contains all matched connections.|[ConnectionQueryResponse](#schemaconnectionqueryresponse)|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -463,16 +470,26 @@ These errors will be returned with the HTTP return code 500.
|---|---|---|---|---|
|body|body|[ShellStartRequest](#schemashellstartrequest)|true|none|
> Example responses
> 400 Response
```json
{
"message": "string"
}
```
<h3 id="start-shell-connection-responses">Responses</h3>
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell session was started.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -489,6 +506,7 @@ const inputBody = '{
}';
const headers = {
'Content-Type':'application/json',
'Accept':'application/json',
'Authorization':'Bearer {access-token}'
};
@ -510,6 +528,7 @@ fetch('http://localhost:21723/shell/start',
import requests
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {access-token}'
}
@ -531,6 +550,7 @@ var request = HttpRequest
.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer {access-token}")
.POST(HttpRequest.BodyPublishers.ofString("""
{
@ -556,6 +576,7 @@ func main() {
headers := map[string][]string{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
"Authorization": []string{"Bearer {access-token}"},
}
@ -573,7 +594,7 @@ func main() {
```shell
# You can also use wget
curl -X POST http://localhost:21723/shell/start \
-H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \
-H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \
--data '
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b"
@ -609,16 +630,26 @@ If the shell is busy or stuck, you might have to work with timeouts to account f
|---|---|---|---|---|
|body|body|[ShellStopRequest](#schemashellstoprequest)|true|none|
> Example responses
> 400 Response
```json
{
"message": "string"
}
```
<h3 id="stop-shell-connection-responses">Responses</h3>
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell session was stopped.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -635,6 +666,7 @@ const inputBody = '{
}';
const headers = {
'Content-Type':'application/json',
'Accept':'application/json',
'Authorization':'Bearer {access-token}'
};
@ -656,6 +688,7 @@ fetch('http://localhost:21723/shell/stop',
import requests
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {access-token}'
}
@ -677,6 +710,7 @@ var request = HttpRequest
.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer {access-token}")
.POST(HttpRequest.BodyPublishers.ofString("""
{
@ -702,6 +736,7 @@ func main() {
headers := map[string][]string{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
"Authorization": []string{"Bearer {access-token}"},
}
@ -719,7 +754,7 @@ func main() {
```shell
# You can also use wget
curl -X POST http://localhost:21723/shell/stop \
-H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \
-H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \
--data '
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b"
@ -777,16 +812,24 @@ However, if any other error occurs like the shell not responding or exiting unex
}
```
> 400 Response
```json
{
"message": "string"
}
```
<h3 id="execute-command-in-a-shell-session-responses">Responses</h3>
|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell command finished.|[ShellExecResponse](#schemashellexecresponse)|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -940,16 +983,24 @@ string
}
```
> 400 Response
```json
{
"message": "string"
}
```
<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|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -1081,16 +1132,26 @@ Writes blob data to a file through an active shell session.
|---|---|---|---|---|
|body|body|[FsWriteRequest](#schemafswriterequest)|true|none|
> Example responses
> 400 Response
```json
{
"message": "string"
}
```
<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|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -1109,6 +1170,7 @@ const inputBody = '{
}';
const headers = {
'Content-Type':'application/json',
'Accept':'application/json',
'Authorization':'Bearer {access-token}'
};
@ -1130,6 +1192,7 @@ fetch('http://localhost:21723/fs/write',
import requests
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer {access-token}'
}
@ -1153,6 +1216,7 @@ var request = HttpRequest
.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer {access-token}")
.POST(HttpRequest.BodyPublishers.ofString("""
{
@ -1180,6 +1244,7 @@ func main() {
headers := map[string][]string{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
"Authorization": []string{"Bearer {access-token}"},
}
@ -1197,7 +1262,7 @@ func main() {
```shell
# You can also use wget
curl -X POST http://localhost:21723/fs/write \
-H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \
-H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \
--data '
{
"connection": "f0ec68aa-63f5-405c-b178-9a4454556d6b",
@ -1245,16 +1310,24 @@ This can be used to run more complex commands on remote systems.
}
```
> 400 Response
```json
{
"message": "string"
}
```
<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|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|[ClientErrorResponse](#schemaclienterrorresponse)|
|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|
|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|[ServerErrorResponse](#schemaservererrorresponse)|
<aside class="warning">
To perform this operation, you must be authenticated by means of one of the following methods:
@ -1783,3 +1856,58 @@ and
|*anonymous*|object|false|none|none|
|» name|string|true|none|The name of the client.|
<h2 id="tocS_ClientErrorResponse">ClientErrorResponse</h2>
<a id="schemaclienterrorresponse"></a>
<a id="schema_ClientErrorResponse"></a>
<a id="tocSclienterrorresponse"></a>
<a id="tocsclienterrorresponse"></a>
```json
{
"message": "string"
}
```
Error returned in case of a client exception
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|message|string|true|none|The error message|
<h2 id="tocS_ServerErrorResponse">ServerErrorResponse</h2>
<a id="schemaservererrorresponse"></a>
<a id="schema_ServerErrorResponse"></a>
<a id="tocSservererrorresponse"></a>
<a id="tocsservererrorresponse"></a>
```json
{
"error": {
"cause": {},
"stackTrace": [],
"suppressed": [],
"localizedMessage": "string",
"message": "string"
}
}
```
Error returned in case of a server exception with HTTP code 500
<h3>Properties</h3>
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|error|object|true|none|The exception information|
|» cause|object|false|none|The exception cause|
|» stackTrace|array|false|none|The java stack trace information|
|» suppressed|array|false|none|Any suppressed exceptions|
|» localizedMessage|string|false|none|Not used|
|» message|string|true|none|The error message|

View file

@ -4,6 +4,7 @@ import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.util.SecretValue;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@ -31,6 +32,7 @@ public class AskpassExchange extends BeaconInterface<AskpassExchange.Request> {
@Builder
@Value
public static class Response {
@NonNull
SecretValue value;
}
}

View file

@ -1,7 +1,9 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.store.FilePath;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
@ -27,5 +29,14 @@ public class ShellStartExchange extends BeaconInterface<ShellStartExchange.Reque
@Jacksonized
@Builder
@Value
public static class Response {}
public static class Response {
@NonNull
ShellDialect shellDialect;
@NonNull
OsType osType;
@NonNull
String osName;
@NonNull
FilePath temp;
}
}

View file

@ -18,6 +18,10 @@ import java.util.function.Function;
public interface ShellControl extends ProcessControl {
void setNonInteractive();
boolean isInteractive();
ElevationHandler getElevationHandler();
void setElevationHandler(ElevationHandler ref);

View file

@ -8,7 +8,6 @@ info:
<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.
Note that this server is HTTP-only for now as it runs only on localhost. HTTPS requests are not accepted.
This allows you to programmatically manage remote systems.
@ -544,11 +543,51 @@ components:
description: The name of the client.
required:
- name
ClientErrorResponse:
description: Error returned in case of a client exception
type: object
properties:
message:
type: string
description: The error message
required:
- message
ServerErrorResponse:
description: Error returned in case of a server exception with HTTP code 500
type: object
properties:
error:
type: object
description: The exception information
properties:
cause:
type: object
description: The exception cause
stackTrace:
type: array
description: The java stack trace information
suppressed:
type: array
description: Any suppressed exceptions
localizedMessage:
type: string
description: Not used
message:
type: string
description: The error message
required:
- message
required:
- error
responses:
Success:
description: The action was successfully performed.
BadRequest:
description: Bad request. Please check error message and your parameters.
content:
application/json:
schema:
$ref: '#/components/schemas/ClientErrorResponse'
Unauthorized:
description: Authorization failed. Please supply a `Bearer` token via
the `Authorization` header.
@ -559,6 +598,10 @@ components:
description: The requested resource could not be found.
InternalServerError:
description: Internal error.
content:
application/json:
schema:
$ref: '#/components/schemas/ServerErrorResponse'
securitySchemes:
bearerAuth:
type: http

View file

@ -1 +1 @@
10.0-9
10.0-10