diff --git a/build.gradle b/build.gradle
index f12aa2d6db3..a81b44604f2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -335,7 +335,7 @@ configure(project(':core')) {
}
configure(project(':cli')) {
- mainClassName = 'bisq.cli.app.BisqCliMain'
+ mainClassName = 'bisq.cli.CliMain'
dependencies {
compile project(':proto')
diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java
new file mode 100644
index 00000000000..27f40b68b6c
--- /dev/null
+++ b/cli/src/main/java/bisq/cli/CliMain.java
@@ -0,0 +1,189 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.cli;
+
+import bisq.proto.grpc.GetBalanceGrpc;
+import bisq.proto.grpc.GetBalanceRequest;
+import bisq.proto.grpc.GetVersionGrpc;
+import bisq.proto.grpc.GetVersionRequest;
+
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.StatusRuntimeException;
+
+import joptsimple.OptionException;
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+
+import java.text.DecimalFormat;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import java.math.BigDecimal;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static java.lang.System.err;
+import static java.lang.System.exit;
+import static java.lang.System.out;
+
+/**
+ * A command-line client for the Bisq gRPC API.
+ */
+@Slf4j
+public class CliMain {
+
+ private static final int EXIT_SUCCESS = 0;
+ private static final int EXIT_FAILURE = 1;
+
+ private enum Method {
+ getversion,
+ getbalance
+ }
+
+ public static void main(String[] args) {
+ var parser = new OptionParser();
+
+ var helpOpt = parser.accepts("help", "Print this help text")
+ .forHelp();
+
+ var hostOpt = parser.accepts("host", "rpc server hostname or IP")
+ .withRequiredArg()
+ .defaultsTo("localhost");
+
+ var portOpt = parser.accepts("port", "rpc server port")
+ .withRequiredArg()
+ .ofType(Integer.class)
+ .defaultsTo(9998);
+
+ var passwordOpt = parser.accepts("password", "rpc server password")
+ .withRequiredArg();
+
+ OptionSet options = null;
+ try {
+ options = parser.parse(args);
+ } catch (OptionException ex) {
+ err.println("Error: " + ex.getMessage());
+ exit(EXIT_FAILURE);
+ }
+
+ if (options.has(helpOpt)) {
+ printHelp(parser, out);
+ exit(EXIT_SUCCESS);
+ }
+
+ @SuppressWarnings("unchecked")
+ var nonOptionArgs = (List) options.nonOptionArguments();
+ if (nonOptionArgs.isEmpty()) {
+ printHelp(parser, err);
+ err.println("Error: no method specified");
+ exit(EXIT_FAILURE);
+ }
+
+ var methodName = nonOptionArgs.get(0);
+ Method method = null;
+ try {
+ method = Method.valueOf(methodName);
+ } catch (IllegalArgumentException ex) {
+ err.printf("Error: '%s' is not a supported method\n", methodName);
+ exit(EXIT_FAILURE);
+ }
+
+ var host = options.valueOf(hostOpt);
+ var port = options.valueOf(portOpt);
+ var password = options.valueOf(passwordOpt);
+ if (password == null) {
+ err.println("Error: missing required 'password' option");
+ exit(EXIT_FAILURE);
+ }
+
+ var credentials = new PasswordCallCredentials(password);
+
+ var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
+ } catch (InterruptedException ex) {
+ ex.printStackTrace(err);
+ exit(EXIT_FAILURE);
+ }
+ }));
+
+ try {
+ switch (method) {
+ case getversion: {
+ var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ var request = GetVersionRequest.newBuilder().build();
+ var version = stub.getVersion(request).getVersion();
+ out.println(version);
+ exit(EXIT_SUCCESS);
+ }
+ case getbalance: {
+ var stub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
+ var request = GetBalanceRequest.newBuilder().build();
+ var balance = stub.getBalance(request).getBalance();
+ if (balance == -1) {
+ err.println("Error: server is still initializing");
+ exit(EXIT_FAILURE);
+ }
+ out.println(formatBalance(balance));
+ exit(EXIT_SUCCESS);
+ }
+ default: {
+ err.printf("Error: unhandled method '%s'\n", method);
+ exit(EXIT_FAILURE);
+ }
+ }
+ } catch (StatusRuntimeException ex) {
+ // This exception is thrown if the client-provided password credentials do not
+ // match the value set on the server. The actual error message is in a nested
+ // exception and we clean it up a bit to make it more presentable.
+ Throwable t = ex.getCause() == null ? ex : ex.getCause();
+ err.println("Error: " + t.getMessage().replace("UNAUTHENTICATED: ", ""));
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ private static void printHelp(OptionParser parser, PrintStream stream) {
+ try {
+ stream.println("Bisq RPC Client");
+ stream.println();
+ stream.println("Usage: bisq-cli [options] ");
+ stream.println();
+ parser.printHelpOn(stream);
+ stream.println();
+ stream.println("Method Description");
+ stream.println("------ -----------");
+ stream.println("getversion Get server version");
+ stream.println("getbalance Get server wallet balance");
+ stream.println();
+ } catch (IOException ex) {
+ ex.printStackTrace(stream);
+ }
+ }
+
+ @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
+ private static String formatBalance(long satoshis) {
+ var btcFormat = new DecimalFormat("###,##0.00000000");
+ var satoshiDivisor = new BigDecimal(100000000);
+ return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor));
+ }
+}
diff --git a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java
new file mode 100644
index 00000000000..14b451d28f8
--- /dev/null
+++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java
@@ -0,0 +1,45 @@
+package bisq.cli;
+
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import io.grpc.Metadata.Key;
+
+import java.util.concurrent.Executor;
+
+import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
+import static io.grpc.Status.UNAUTHENTICATED;
+import static java.lang.String.format;
+
+/**
+ * Sets the {@value PASSWORD_KEY} rpc call header to a given value.
+ */
+class PasswordCallCredentials extends CallCredentials {
+
+ public static final String PASSWORD_KEY = "password";
+
+ private final String passwordValue;
+
+ public PasswordCallCredentials(String passwordValue) {
+ if (passwordValue == null)
+ throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY));
+ this.passwordValue = passwordValue;
+ }
+
+ @Override
+ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) {
+ appExecutor.execute(() -> {
+ try {
+ var headers = new Metadata();
+ var passwordKey = Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER);
+ headers.put(passwordKey, passwordValue);
+ metadataApplier.apply(headers);
+ } catch (Throwable ex) {
+ metadataApplier.fail(UNAUTHENTICATED.withCause(ex));
+ }
+ });
+ }
+
+ @Override
+ public void thisUsesUnstableApi() {
+ }
+}
diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java
deleted file mode 100644
index 015051bdc32..00000000000
--- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * This file is part of Bisq.
- *
- * Bisq is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or (at
- * your option) any later version.
- *
- * Bisq is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
- * License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Bisq. If not, see .
- */
-
-package bisq.cli.app;
-
-import io.grpc.ManagedChannel;
-import io.grpc.ManagedChannelBuilder;
-
-import joptsimple.OptionParser;
-import joptsimple.OptionSet;
-import joptsimple.OptionSpec;
-
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import lombok.extern.slf4j.Slf4j;
-
-import static bisq.cli.app.CommandParser.GETBALANCE;
-import static bisq.cli.app.CommandParser.GETVERSION;
-import static bisq.cli.app.CommandParser.HELP;
-import static bisq.cli.app.CommandParser.STOPSERVER;
-import static java.lang.String.format;
-import static java.lang.System.exit;
-import static java.lang.System.out;
-
-/**
- * gRPC client.
- */
-@Slf4j
-public class BisqCliMain {
-
- private static final int EXIT_SUCCESS = 0;
- private static final int EXIT_FAILURE = 1;
-
- private final ManagedChannel channel;
- private final CliCommand cmd;
- private final OptionParser parser;
-
- public static void main(String[] args) {
- new BisqCliMain("localhost", 9998, args);
- }
-
- private BisqCliMain(String host, int port, String[] args) {
- // Channels are secure by default (via SSL/TLS); for the example disable TLS to avoid needing certificates.
- this(ManagedChannelBuilder.forAddress(host, port).usePlaintext().build());
- String command = parseCommand(args);
- String result = runCommand(command);
- out.println(result);
- try {
- shutdown(); // Orderly channel shutdown
- } catch (InterruptedException ignored) {
- }
- }
-
- /**
- * Construct client for accessing server using the existing channel.
- */
- private BisqCliMain(ManagedChannel channel) {
- this.channel = channel;
- this.cmd = new CliCommand(channel);
- this.parser = new CommandParser().configure();
- }
-
- private String runCommand(String command) {
- final String result;
- switch (command) {
- case HELP:
- CommandParser.printHelp();
- exit(EXIT_SUCCESS);
- case GETBALANCE:
- long satoshis = cmd.getBalance();
- result = satoshis == -1 ? "Server initializing..." : cmd.prettyBalance.apply(satoshis);
- break;
- case GETVERSION:
- result = cmd.getVersion();
- break;
- case STOPSERVER:
- cmd.stopServer();
- result = "Server stopped";
- break;
- default:
- result = format("Unknown command '%s'", command);
- }
- return result;
- }
-
- private String parseCommand(String[] params) {
- OptionSpec nonOptions = parser.nonOptions().ofType(String.class);
- OptionSet options = parser.parse(params);
- List detectedOptions = nonOptions.values(options);
- if (detectedOptions.isEmpty()) {
- CommandParser.printHelp();
- exit(EXIT_FAILURE);
- }
- return detectedOptions.get(0);
- }
-
- private void shutdown() throws InterruptedException {
- channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
- exit(EXIT_SUCCESS);
- }
-}
diff --git a/cli/src/main/java/bisq/cli/app/CliCommand.java b/cli/src/main/java/bisq/cli/app/CliCommand.java
deleted file mode 100644
index e3b0bc813fe..00000000000
--- a/cli/src/main/java/bisq/cli/app/CliCommand.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package bisq.cli.app;
-
-import bisq.proto.grpc.GetBalanceGrpc;
-import bisq.proto.grpc.GetBalanceRequest;
-import bisq.proto.grpc.GetVersionGrpc;
-import bisq.proto.grpc.GetVersionRequest;
-import bisq.proto.grpc.StopServerGrpc;
-import bisq.proto.grpc.StopServerRequest;
-
-import io.grpc.ManagedChannel;
-import io.grpc.StatusRuntimeException;
-
-import java.text.DecimalFormat;
-
-import java.math.BigDecimal;
-
-import java.util.function.Function;
-
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-final class CliCommand {
-
- private final GetBalanceGrpc.GetBalanceBlockingStub getBalanceStub;
- private final GetVersionGrpc.GetVersionBlockingStub getVersionStub;
- private final StopServerGrpc.StopServerBlockingStub stopServerStub;
-
- private final DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000");
- private final BigDecimal satoshiDivisor = new BigDecimal(100000000);
- @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
- final Function prettyBalance = (sats) -> btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor));
-
- CliCommand(ManagedChannel channel) {
- getBalanceStub = GetBalanceGrpc.newBlockingStub(channel);
- getVersionStub = GetVersionGrpc.newBlockingStub(channel);
- stopServerStub = StopServerGrpc.newBlockingStub(channel);
- }
-
- String getVersion() {
- GetVersionRequest request = GetVersionRequest.newBuilder().build();
- try {
- return getVersionStub.getVersion(request).getVersion();
- } catch (StatusRuntimeException e) {
- return "RPC failed: " + e.getStatus();
- }
- }
-
- long getBalance() {
- GetBalanceRequest request = GetBalanceRequest.newBuilder().build();
- try {
- return getBalanceStub.getBalance(request).getBalance();
- } catch (StatusRuntimeException e) {
- log.warn("RPC failed: {}", e.getStatus());
- return -1;
- }
- }
-
- void stopServer() {
- StopServerRequest request = StopServerRequest.newBuilder().build();
- try {
- stopServerStub.stopServer(request);
- } catch (StatusRuntimeException e) {
- log.warn("RPC failed: {}", e.getStatus());
- }
- }
-}
diff --git a/cli/src/main/java/bisq/cli/app/CommandParser.java b/cli/src/main/java/bisq/cli/app/CommandParser.java
deleted file mode 100644
index b1615d81165..00000000000
--- a/cli/src/main/java/bisq/cli/app/CommandParser.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package bisq.cli.app;
-
-import joptsimple.OptionParser;
-
-import static java.lang.System.out;
-
-final class CommandParser {
-
- // Option name constants
- static final String HELP = "help";
- static final String GETBALANCE = "getbalance";
- static final String GETVERSION = "getversion";
- static final String STOPSERVER = "stopserver";
-
- OptionParser configure() {
- OptionParser parser = new OptionParser();
- parser.allowsUnrecognizedOptions();
- parser.nonOptions(GETBALANCE).ofType(String.class).describedAs("get btc balance");
- parser.nonOptions(GETVERSION).ofType(String.class).describedAs("get bisq version");
- return parser;
- }
-
- static void printHelp() {
- out.println("Usage: bisq-cli getbalance | getversion");
- }
-
-}
diff --git a/cli/test.sh b/cli/test.sh
new file mode 100755
index 00000000000..046cbd910aa
--- /dev/null
+++ b/cli/test.sh
@@ -0,0 +1,156 @@
+#!/bin/bash
+#
+# References & examples for expect:
+#
+# - https://pantz.org/software/expect/expect_examples_and_tips.html
+# - https://stackoverflow.com/questions/13982310/else-string-matching-in-expect
+# - https://gist.github.com/Fluidbyte/6294378
+# - https://www.oreilly.com/library/view/exploring-expect/9781565920903/ch04.html
+#
+# Prior to running this script, run:
+#
+# ./bisq-daemon --apiPassword=xyz
+#
+# The data directory used must contain an unencrypted wallet with a 0 BTC balance
+
+# Ensure project root is the current working directory
+cd $(dirname $0)/..
+
+OUTPUT=$(expect -c '
+ # exp_internal 1
+ puts "TEST unsupported cmd error"
+ set expected "Error: '\''bogus'\'' is not a supported method"
+ spawn ./bisq-cli --password=xyz bogus
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ puts "TEST unrecognized option error"
+ set expected "Error: bogus is not a recognized option"
+ spawn ./bisq-cli --bogus getversion
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ # exp_internal 1
+ puts "TEST missing required password option error"
+ set expected "Error: missing required '\''password'\'' option"
+ spawn ./bisq-cli getversion
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ # exp_internal 1
+ puts "TEST getversion (incorrect password error)"
+ set expected "Error: incorrect '\''password'\'' rpc header value"
+ spawn ./bisq-cli --password=bogus getversion
+ expect {
+ $expected { puts "PASS\n" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ # exp_internal 1
+ puts "TEST getversion (password value in quotes)"
+ set expected "1.3.2"
+ # Note: have to define quoted argument in a variable as "''value''"
+ set pwd_in_quotes "''xyz''"
+ spawn ./bisq-cli --password=$pwd_in_quotes getversion
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ puts "TEST getversion"
+ set expected "1.3.2"
+ spawn ./bisq-cli --password=xyz getversion
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ puts "TEST getbalance"
+ # exp_internal 1
+ set expected "0.00000000"
+ spawn ./bisq-cli --password=xyz getbalance
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+OUTPUT=$(expect -c '
+ puts "TEST running with no options or arguments prints help text"
+ # exp_internal 1
+ set expected "Bisq RPC Client"
+ spawn ./bisq-cli
+ expect {
+ $expected { puts "PASS" }
+ default {
+ set results $expect_out(buffer)
+ puts "FAIL expected = $expected"
+ puts " actual = $results"
+ }
+ }
+')
+echo "$OUTPUT"
+echo "========================================================================"
+
+echo "TEST --help option prints help text"
+./bisq-cli --help
diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java
index b959d4ec1c4..b04c58e73ef 100644
--- a/common/src/main/java/bisq/common/config/Config.java
+++ b/common/src/main/java/bisq/common/config/Config.java
@@ -116,6 +116,7 @@ public class Config {
public static final String DAO_ACTIVATED = "daoActivated";
public static final String DUMP_DELAYED_PAYOUT_TXS = "dumpDelayedPayoutTxs";
public static final String ALLOW_FAULTY_DELAYED_TXS = "allowFaultyDelayedTxs";
+ public static final String API_PASSWORD = "apiPassword";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@@ -140,7 +141,7 @@ public class Config {
public final boolean helpRequested;
public final File configFile;
- // Options supported both at the cli and in the config file
+ // Options supported on cmd line and in the config file
public final String appName;
public final File userDataDir;
public final File appDataDir;
@@ -199,6 +200,7 @@ public class Config {
public final long genesisTotalSupply;
public final boolean dumpDelayedPayoutTxs;
public final boolean allowFaultyDelayedTxs;
+ public final String apiPassword;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@@ -206,7 +208,7 @@ public class Config {
public final File storageDir;
public final File keyStorageDir;
- // The parser that will be used to parse both cli and config file options
+ // The parser that will be used to parse both cmd line and config file options
private final OptionParser parser = new OptionParser();
/**
@@ -615,6 +617,11 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) {
.ofType(boolean.class)
.defaultsTo(false);
+ ArgumentAcceptingOptionSpec apiPasswordOpt =
+ parser.accepts(API_PASSWORD, "gRPC API password")
+ .withRequiredArg()
+ .defaultsTo("");
+
try {
CompositeOptionSet options = new CompositeOptionSet();
@@ -727,6 +734,7 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) {
this.daoActivated = options.valueOf(daoActivatedOpt);
this.dumpDelayedPayoutTxs = options.valueOf(dumpDelayedPayoutTxsOpt);
this.allowFaultyDelayedTxs = options.valueOf(allowFaultyDelayedTxsOpt);
+ this.apiPassword = options.valueOf(apiPasswordOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),
diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java
index ef6631462f9..9ce22b72db8 100644
--- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java
+++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java
@@ -22,6 +22,8 @@
import bisq.core.trade.handlers.TransactionResultHandler;
import bisq.core.trade.statistics.TradeStatistics2;
+import bisq.common.config.Config;
+
import bisq.proto.grpc.GetBalanceGrpc;
import bisq.proto.grpc.GetBalanceReply;
import bisq.proto.grpc.GetBalanceRequest;
@@ -65,6 +67,7 @@ public class BisqGrpcServer {
private Server server;
private static BisqGrpcServer instance;
+ private static Config config;
private static CoreApi coreApi;
@@ -170,9 +173,10 @@ public void stopServer(StopServerRequest req, StreamObserver re
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
- public BisqGrpcServer(CoreApi coreApi) {
+ public BisqGrpcServer(Config config, CoreApi coreApi) {
instance = this;
+ BisqGrpcServer.config = config;
BisqGrpcServer.coreApi = coreApi;
try {
@@ -211,15 +215,16 @@ private void start() throws IOException {
.addService(new GetPaymentAccountsImpl())
.addService(new PlaceOfferImpl())
.addService(new StopServerImpl())
+ .intercept(new PasswordAuthInterceptor(config.apiPassword))
.build()
.start();
log.info("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
- log.error("*** shutting down gRPC server since JVM is shutting down");
+ log.error("Shutting down gRPC server");
BisqGrpcServer.this.stop();
- log.error("*** server shut down");
+ log.error("Server shut down");
}));
}
}
diff --git a/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java
new file mode 100644
index 00000000000..2ab29bcdc95
--- /dev/null
+++ b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java
@@ -0,0 +1,45 @@
+package bisq.core.grpc;
+
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import io.grpc.StatusRuntimeException;
+
+import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
+import static io.grpc.Metadata.Key;
+import static io.grpc.Status.UNAUTHENTICATED;
+import static java.lang.String.format;
+
+/**
+ * Authorizes rpc server calls by comparing the value of the caller's
+ * {@value PASSWORD_KEY} header to an expected value set at server startup time.
+ *
+ * @see bisq.common.config.Config#apiPassword
+ */
+class PasswordAuthInterceptor implements ServerInterceptor {
+
+ public static final String PASSWORD_KEY = "password";
+
+ private final String expectedPasswordValue;
+
+ public PasswordAuthInterceptor(String expectedPasswordValue) {
+ this.expectedPasswordValue = expectedPasswordValue;
+ }
+
+ @Override
+ public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata headers,
+ ServerCallHandler serverCallHandler) {
+ var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER));
+
+ if (actualPasswordValue == null)
+ throw new StatusRuntimeException(UNAUTHENTICATED.withDescription(
+ format("missing '%s' rpc header value", PASSWORD_KEY)));
+
+ if (!actualPasswordValue.equals(expectedPasswordValue))
+ throw new StatusRuntimeException(UNAUTHENTICATED.withDescription(
+ format("incorrect '%s' rpc header value", PASSWORD_KEY)));
+
+ return serverCallHandler.startCall(serverCall, headers);
+ }
+}
diff --git a/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java b/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java
index bcdd930d006..1e19ed23c5e 100644
--- a/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java
+++ b/daemon/src/main/java/bisq/daemon/app/BisqDaemonMain.java
@@ -99,6 +99,6 @@ protected void onApplicationStarted() {
super.onApplicationStarted();
CoreApi coreApi = injector.getInstance(CoreApi.class);
- new BisqGrpcServer(coreApi);
+ new BisqGrpcServer(config, coreApi);
}
}
diff --git a/daemon/src/main/java/resources/logback.xml b/daemon/src/main/resources/logback.xml
similarity index 100%
rename from daemon/src/main/java/resources/logback.xml
rename to daemon/src/main/resources/logback.xml