diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java index c5f0fc4c9..272427578 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -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(); } } 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 index 043f67e85..82d9a2ac2 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java @@ -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(); } } 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 0471fb36e..7b626a67d 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 @@ -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(); } } diff --git a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java index 18243a547..89a350d6a 100644 --- a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java +++ b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java @@ -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); } } diff --git a/app/src/main/java/io/xpipe/app/util/SecretManager.java b/app/src/main/java/io/xpipe/app/util/SecretManager.java index 61bf2b4ec..b754460e9 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretManager.java +++ b/app/src/main/java/io/xpipe/app/util/SecretManager.java @@ -34,8 +34,9 @@ public class SecretManager { List suppliers, SecretQuery fallback, List 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); diff --git a/app/src/main/java/io/xpipe/app/util/SecretQuery.java b/app/src/main/java/io/xpipe/app/util/SecretQuery.java index 2897b7a18..99afbcfcd 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretQuery.java +++ b/app/src/main/java/io/xpipe/app/util/SecretQuery.java @@ -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 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); diff --git a/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java b/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java index b7578cac6..5ffed79d6 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java +++ b/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java @@ -22,7 +22,8 @@ public class SecretQueryProgress { private final List filters; private final List 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 suppliers, @NonNull SecretQuery fallback, @NonNull List 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; } diff --git a/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java b/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java index 5ba63e644..72b80c02f 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java +++ b/app/src/main/java/io/xpipe/app/util/SecretQueryResult.java @@ -8,5 +8,5 @@ import lombok.Value; public class SecretQueryResult { SecretValue secret; - boolean cancelled; + SecretQueryState state; } diff --git a/app/src/main/java/io/xpipe/app/util/SecretQueryState.java b/app/src/main/java/io/xpipe/app/util/SecretQueryState.java new file mode 100644 index 000000000..04afd249f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretQueryState.java @@ -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"; + } + }; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java index 2d5c3b1bf..b50df1008 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java +++ b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java @@ -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); } } 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 d53af5952..6363c135f 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 @@ -29,7 +29,6 @@ You can get started by either using this page as an API reference or alternative 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. 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)|