From fe9e57babb3c31d6cd928db2dbc73d17d9b7656d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 22 Apr 2020 18:16:01 -0300 Subject: [PATCH 01/40] Implement simple cleartext gRPC authentication * Renamed CliCommand to RpcCommand, differentiating it from cmd string token(s) * Added new CliConfig, based on :common Config * Injected Config into CoreApi to make bisq.properties and cmd line args available to server * Added cleartext username:password BisqCallCredentials to :cli, and an AuthenticationInterceptor to :core (server) * Moved :daemon resources folder to expected location under src/main * Duplicated CompositeOptionSet, ConfigException and BisqException in :cli because they are not visible from :common. (TODO refactor?) CompositeOptionSet will be used to let command line parameters override params set in config file. * Removed outdated references to :cli in a couple of comments in :common Config * gRPC parameters & command names are lowercase to mimic bitcoind/rpc TBD * Decide what rpc auth param names to use, to differentiate them from bitcoind rpc user/passord param names --- .../bisq/cli/app/BisqCallCredentials.java | 55 ++++++++++++ .../main/java/bisq/cli/app/BisqCliMain.java | 87 +++++++++---------- .../main/java/bisq/cli/app/BisqException.java | 16 ++++ cli/src/main/java/bisq/cli/app/CliConfig.java | 74 ++++++++++++++++ .../main/java/bisq/cli/app/CommandParser.java | 62 ++++++++++--- .../java/bisq/cli/app/CompositeOptionSet.java | 56 ++++++++++++ .../java/bisq/cli/app/ConfigException.java | 9 ++ .../app/{CliCommand.java => RpcCommand.java} | 12 +-- .../main/java/bisq/common/config/Config.java | 4 +- .../core/grpc/AuthenticationInterceptor.java | 62 +++++++++++++ .../java/bisq/core/grpc/BisqGrpcServer.java | 7 +- .../src/main/java/bisq/core/grpc/CoreApi.java | 10 ++- .../src/main/{java => }/resources/logback.xml | 0 13 files changed, 382 insertions(+), 72 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/app/BisqCallCredentials.java create mode 100644 cli/src/main/java/bisq/cli/app/BisqException.java create mode 100644 cli/src/main/java/bisq/cli/app/CliConfig.java create mode 100644 cli/src/main/java/bisq/cli/app/CompositeOptionSet.java create mode 100644 cli/src/main/java/bisq/cli/app/ConfigException.java rename cli/src/main/java/bisq/cli/app/{CliCommand.java => RpcCommand.java} (78%) create mode 100644 core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java rename daemon/src/main/{java => }/resources/logback.xml (100%) diff --git a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java new file mode 100644 index 00000000000..1ddf89f9be9 --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java @@ -0,0 +1,55 @@ +package bisq.cli.app; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; + +import java.util.Map; +import java.util.concurrent.Executor; + +import static bisq.cli.app.CliConfig.RPC_PASSWORD; +import static bisq.cli.app.CliConfig.RPC_USER; +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Status.UNAUTHENTICATED; + +/** + * Simple credentials implementation for sending cleartext username:password token via rpc call headers. + */ +public class BisqCallCredentials extends CallCredentials { + + private final Map credentials; + + public BisqCallCredentials(Map credentials) { + this.credentials = credentials; + } + + @Override + public void applyRequestMetadata( + RequestInfo requestInfo, + Executor appExecutor, + MetadataApplier metadataApplier) { + + appExecutor.execute(() -> { + try { + Metadata headers = new Metadata(); + Key creds = Key.of("bisqd-creds", ASCII_STRING_MARSHALLER); + headers.put(creds, encodeCredentials()); + metadataApplier.apply(headers); + } catch (Throwable e) { + metadataApplier.fail(UNAUTHENTICATED.withCause(e)); + } + }); + } + + @Override + public void thisUsesUnstableApi() { + } + + private String encodeCredentials() { + if (!credentials.containsKey(RPC_USER) || !credentials.containsKey(RPC_PASSWORD)) { + throw new ConfigException("Cannot call rpc service without username:password credentials"); + } else { + return credentials.get(RPC_USER) + ":" + credentials.get(RPC_PASSWORD); + } + } +} diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java index 015051bdc32..89be7adb0d0 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -20,19 +20,14 @@ 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 bisq.cli.app.CliConfig.GETBALANCE; +import static bisq.cli.app.CliConfig.GETVERSION; +import static bisq.cli.app.CliConfig.HELP; +import static bisq.cli.app.CliConfig.STOPSERVER; import static java.lang.String.format; import static java.lang.System.exit; import static java.lang.System.out; @@ -47,18 +42,17 @@ public class BisqCliMain { private static final int EXIT_FAILURE = 1; private final ManagedChannel channel; - private final CliCommand cmd; - private final OptionParser parser; + private final RpcCommand rpcCommand; + private final CliConfig config; + private final CommandParser 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); + this(ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(), args); + String result = runCommand(); out.println(result); try { shutdown(); // Orderly channel shutdown @@ -69,44 +63,41 @@ private BisqCliMain(String host, int port, String[] args) { /** * Construct client for accessing server using the existing channel. */ - private BisqCliMain(ManagedChannel channel) { + private BisqCliMain(ManagedChannel channel, String[] args) { 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; + this.config = new CliConfig(args); + this.parser = new CommandParser(config); + this.rpcCommand = new RpcCommand(channel, parser); } - 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(); + private String runCommand() { + if (parser.getCmdToken().isPresent()) { + final String cmdToken = parser.getCmdToken().get(); + final String result; + switch (cmdToken) { + case HELP: + CliConfig.printHelp(); + exit(EXIT_SUCCESS); + case GETBALANCE: + long satoshis = rpcCommand.getBalance(); + result = satoshis == -1 ? "Server initializing..." : rpcCommand.prettyBalance.apply(satoshis); + break; + case GETVERSION: + result = rpcCommand.getVersion(); + break; + case STOPSERVER: + rpcCommand.stopServer(); + result = "Server stopped"; + break; + default: + result = format("Unknown command '%s'", cmdToken); + } + return result; + } else { + CliConfig.printHelp(); exit(EXIT_FAILURE); + return null; } - return detectedOptions.get(0); } private void shutdown() throws InterruptedException { diff --git a/cli/src/main/java/bisq/cli/app/BisqException.java b/cli/src/main/java/bisq/cli/app/BisqException.java new file mode 100644 index 00000000000..0248a9d9b38 --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/BisqException.java @@ -0,0 +1,16 @@ +package bisq.cli.app; + +public class BisqException extends RuntimeException { + + public BisqException(Throwable cause) { + super(cause); + } + + public BisqException(String format, Object... args) { + super(String.format(format, args)); + } + + public BisqException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } +} diff --git a/cli/src/main/java/bisq/cli/app/CliConfig.java b/cli/src/main/java/bisq/cli/app/CliConfig.java new file mode 100644 index 00000000000..dbe2d1af333 --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/CliConfig.java @@ -0,0 +1,74 @@ +package bisq.cli.app; + +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import static java.lang.System.out; + +final class CliConfig { + + // Non-Option argument name constants + static final String HELP = "help"; + static final String GETBALANCE = "getbalance"; + static final String GETVERSION = "getversion"; + static final String STOPSERVER = "stopserver"; + + // Argument accepting name constants + static final String RPC_USER = "rpcuser"; + static final String RPC_PASSWORD = "rpcpassword"; + + // Argument accepting cmd options + final String rpcUser; + final String rpcPassword; + + // The parser that will be used to parse both cmd line and config file options + private final OptionParser optionParser = new OptionParser(); + private final String[] params; + + CliConfig(String[] params) { + this.params = params; + + ArgumentAcceptingOptionSpec rpcUserOpt = + optionParser.accepts(RPC_USER, "Bisq daemon username") + .withRequiredArg() + .defaultsTo(""); + ArgumentAcceptingOptionSpec rpcPasswordOpt = + optionParser.accepts(RPC_PASSWORD, "Bisq daemon password") + .withRequiredArg() + .defaultsTo(""); + try { + CompositeOptionSet options = new CompositeOptionSet(); + // Parse command line options + OptionSet cliOpts = optionParser.parse(params); + options.addOptionSet(cliOpts); + + this.rpcUser = options.valueOf(rpcUserOpt); + this.rpcPassword = options.valueOf(rpcPasswordOpt); + + optionParser.allowsUnrecognizedOptions(); + optionParser.nonOptions(GETBALANCE).ofType(String.class).describedAs("Get btc balance"); + optionParser.nonOptions(GETVERSION).ofType(String.class).describedAs("Get bisq version"); + + } catch (OptionException ex) { + throw new ConfigException("Problem parsing option '%s': %s", + ex.options().get(0), + ex.getCause() != null ? + ex.getCause().getMessage() : + ex.getMessage()); + } + } + + OptionParser getOptionParser() { + return this.optionParser; + } + + public String[] getParams() { + return this.params; + } + + static void printHelp() { + out.println("Usage: bisq-cli --rpcpassword=user --rpcpassword=password getbalance | getversion"); + } +} diff --git a/cli/src/main/java/bisq/cli/app/CommandParser.java b/cli/src/main/java/bisq/cli/app/CommandParser.java index b1615d81165..827dd4d4041 100644 --- a/cli/src/main/java/bisq/cli/app/CommandParser.java +++ b/cli/src/main/java/bisq/cli/app/CommandParser.java @@ -1,27 +1,61 @@ package bisq.cli.app; import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; 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; + private Optional cmdToken; + private final Map creds = new HashMap<>(); + private final CliConfig config; + + public CommandParser(CliConfig config) { + this.config = config; + init(); + } + + public Optional getCmdToken() { + return this.cmdToken; } - static void printHelp() { - out.println("Usage: bisq-cli getbalance | getversion"); + public Map getCreds() { + return this.creds; } + private void init() { + OptionParser parser = config.getOptionParser(); + OptionSpec nonOptions = parser.nonOptions().ofType(String.class); + OptionSet options = parser.parse(config.getParams()); + creds.putAll(rpcCredentials.apply(options)); + // debugOptionsSet(options, nonOptions); + List detectedOptions = nonOptions.values(options); + cmdToken = detectedOptions.isEmpty() ? Optional.empty() : Optional.of(detectedOptions.get(0)); + } + + final Function> rpcCredentials = (opts) -> + opts.asMap().entrySet().stream() + .filter(e -> e.getKey().options().size() == 1 && e.getKey().options().get(0).startsWith("rpc")) + .collect(Collectors.toUnmodifiableMap(m -> m.getKey().options().get(0), m -> (String) m.getValue().get(0))); + + private void debugOptionsSet(OptionSet options, OptionSpec nonOptions) { + // https://programtalk.com/java-api-usage-examples/joptsimple.OptionParser + out.println("*** BEGIN Debug OptionSet ***"); + out.println("[argument acceptors]"); + options.asMap().entrySet().forEach(out::println); + out.println("[rpc credentials map]"); + out.println(rpcCredentials.apply(options)); + out.println("[non options]"); + nonOptions.values(options).forEach(out::println); + out.println("*** END Debug OptionSet ***"); + } } diff --git a/cli/src/main/java/bisq/cli/app/CompositeOptionSet.java b/cli/src/main/java/bisq/cli/app/CompositeOptionSet.java new file mode 100644 index 00000000000..ffea7fe01b3 --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/CompositeOptionSet.java @@ -0,0 +1,56 @@ +package bisq.cli.app; + + +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.util.ArrayList; +import java.util.List; + +/** + * Composes multiple JOptSimple {@link OptionSet} instances such that calls to + * {@link #valueOf(OptionSpec)} and co will search all instances in the order they were + * added and return any value explicitly set, otherwise returning the default value for + * the given option or null if no default has been set. The API found here loosely + * emulates the {@link OptionSet} API without going through the unnecessary work of + * actually extending it. In practice, this class is used to compose options provided at + * the command line with those provided via config file, such that those provided at the + * command line take precedence over those provided in the config file. + */ +class CompositeOptionSet { + + private final List optionSets = new ArrayList<>(); + + public void addOptionSet(OptionSet optionSet) { + optionSets.add(optionSet); + } + + public boolean has(OptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return true; + + return false; + } + + public V valueOf(OptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return optionSet.valueOf(option); + + // None of the provided option sets specified the given option so fall back to + // the default value (if any) provided by the first specified OptionSet + return optionSets.get(0).valueOf(option); + } + + public List valuesOf(ArgumentAcceptingOptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return optionSet.valuesOf(option); + + // None of the provided option sets specified the given option so fall back to + // the default value (if any) provided by the first specified OptionSet + return optionSets.get(0).valuesOf(option); + } +} diff --git a/cli/src/main/java/bisq/cli/app/ConfigException.java b/cli/src/main/java/bisq/cli/app/ConfigException.java new file mode 100644 index 00000000000..3ea785f2b16 --- /dev/null +++ b/cli/src/main/java/bisq/cli/app/ConfigException.java @@ -0,0 +1,9 @@ +package bisq.cli.app; + +public class ConfigException extends BisqException { + + public ConfigException(String format, Object... args) { + super(format, args); + } + +} diff --git a/cli/src/main/java/bisq/cli/app/CliCommand.java b/cli/src/main/java/bisq/cli/app/RpcCommand.java similarity index 78% rename from cli/src/main/java/bisq/cli/app/CliCommand.java rename to cli/src/main/java/bisq/cli/app/RpcCommand.java index e3b0bc813fe..b8455e7711f 100644 --- a/cli/src/main/java/bisq/cli/app/CliCommand.java +++ b/cli/src/main/java/bisq/cli/app/RpcCommand.java @@ -19,8 +19,9 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -final class CliCommand { +final class RpcCommand { + private final BisqCallCredentials callCredentials; private final GetBalanceGrpc.GetBalanceBlockingStub getBalanceStub; private final GetVersionGrpc.GetVersionBlockingStub getVersionStub; private final StopServerGrpc.StopServerBlockingStub stopServerStub; @@ -30,10 +31,11 @@ final class CliCommand { @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); + RpcCommand(ManagedChannel channel, CommandParser parser) { + this.callCredentials = new BisqCallCredentials(parser.getCreds()); + this.getBalanceStub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); + this.getVersionStub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); + this.stopServerStub = StopServerGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); } String getVersion() { diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index b959d4ec1c4..ddefb748bab 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -140,7 +140,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; @@ -206,7 +206,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(); /** diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java new file mode 100644 index 00000000000..a4008583786 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java @@ -0,0 +1,62 @@ +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 java.util.function.Predicate; + +import lombok.extern.slf4j.Slf4j; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Metadata.Key; +import static io.grpc.Status.UNAUTHENTICATED; + +/** + * Simple authentication interceptor to validate a cleartext token in username:password format. + */ +@Slf4j +public class AuthenticationInterceptor implements ServerInterceptor { + + private String rpcUser, rpcPassword; + + public AuthenticationInterceptor(String rpcUser, String rpcPassword) { + this.rpcUser = rpcUser; + this.rpcPassword = rpcPassword; + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall serverCall, + Metadata metadata, + ServerCallHandler serverCallHandler) { + authenticate(metadata); + return serverCallHandler.startCall(serverCall, metadata); + } + + private void authenticate(Metadata metadata) { + final String authToken = metadata.get(Key.of("bisqd-creds", ASCII_STRING_MARSHALLER)); + if (authToken == null) { + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Authentication token is missing")); + } else { + try { + if (isValidToken.test(authToken)) { + log.info("Authenticated user {} with token {}", rpcUser, authToken); + } else { + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid username or password")); + } + } catch (Exception e) { + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e)); + } + } + } + + private final Predicate isValidUser = (u) -> u.equals(rpcUser); + private final Predicate isValidPassword = (p) -> p.equals(rpcPassword); + private final Predicate isValidToken = (t) -> { + String[] pair = t.split(":"); + return isValidUser.test(pair[0]) && isValidPassword.test(pair[1]); + }; +} diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java index ef6631462f9..d595b23a855 100644 --- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -201,6 +201,8 @@ public void stop() { private void start() throws IOException { // TODO add to options int port = 9998; + String rpcUser = coreApi.getConfig().rpcUser; + String rpcPassword = coreApi.getConfig().rpcPassword; // Config services server = ServerBuilder.forPort(port) @@ -211,15 +213,16 @@ private void start() throws IOException { .addService(new GetPaymentAccountsImpl()) .addService(new PlaceOfferImpl()) .addService(new StopServerImpl()) + .intercept(new AuthenticationInterceptor(rpcUser, rpcPassword)) .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/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index 2877849147f..737b592609b 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -32,6 +32,7 @@ import bisq.core.user.User; import bisq.common.app.Version; +import bisq.common.config.Config; import org.bitcoinj.core.Coin; @@ -49,6 +50,7 @@ */ @Slf4j public class CoreApi { + private final Config config; private final Balances balances; private final BalancePresentation balancePresentation; private final OfferBookService offerBookService; @@ -58,13 +60,15 @@ public class CoreApi { private final User user; @Inject - public CoreApi(Balances balances, + public CoreApi(Config config, + Balances balances, BalancePresentation balancePresentation, OfferBookService offerBookService, TradeStatisticsManager tradeStatisticsManager, CreateOfferService createOfferService, OpenOfferManager openOfferManager, User user) { + this.config = config; this.balances = balances; this.balancePresentation = balancePresentation; this.offerBookService = offerBookService; @@ -74,6 +78,10 @@ public CoreApi(Balances balances, this.user = user; } + public Config getConfig() { + return config; + } + public String getVersion() { return Version.VERSION; } 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 From 506e12da4679f44d518edaea7479cc72b6197a12 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 09:50:08 +0200 Subject: [PATCH 02/40] Inject Config directly into BisqGrpcServer There is no need or benefit to injecting Config into CoreApi for the purpose of accessing it indirectly through BisqGrpcServer. --- core/src/main/java/bisq/core/grpc/BisqGrpcServer.java | 10 +++++++--- core/src/main/java/bisq/core/grpc/CoreApi.java | 10 +--------- .../src/main/java/bisq/daemon/app/BisqDaemonMain.java | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java index d595b23a855..aeee2cf9e2a 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 { @@ -201,8 +205,8 @@ public void stop() { private void start() throws IOException { // TODO add to options int port = 9998; - String rpcUser = coreApi.getConfig().rpcUser; - String rpcPassword = coreApi.getConfig().rpcPassword; + String rpcUser = config.rpcUser; + String rpcPassword = config.rpcPassword; // Config services server = ServerBuilder.forPort(port) diff --git a/core/src/main/java/bisq/core/grpc/CoreApi.java b/core/src/main/java/bisq/core/grpc/CoreApi.java index 737b592609b..2877849147f 100644 --- a/core/src/main/java/bisq/core/grpc/CoreApi.java +++ b/core/src/main/java/bisq/core/grpc/CoreApi.java @@ -32,7 +32,6 @@ import bisq.core.user.User; import bisq.common.app.Version; -import bisq.common.config.Config; import org.bitcoinj.core.Coin; @@ -50,7 +49,6 @@ */ @Slf4j public class CoreApi { - private final Config config; private final Balances balances; private final BalancePresentation balancePresentation; private final OfferBookService offerBookService; @@ -60,15 +58,13 @@ public class CoreApi { private final User user; @Inject - public CoreApi(Config config, - Balances balances, + public CoreApi(Balances balances, BalancePresentation balancePresentation, OfferBookService offerBookService, TradeStatisticsManager tradeStatisticsManager, CreateOfferService createOfferService, OpenOfferManager openOfferManager, User user) { - this.config = config; this.balances = balances; this.balancePresentation = balancePresentation; this.offerBookService = offerBookService; @@ -78,10 +74,6 @@ public CoreApi(Config config, this.user = user; } - public Config getConfig() { - return config; - } - public String getVersion() { return Version.VERSION; } 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); } } From 3fba97cefc0fcc12e6df5b614246b7c13108785e Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 09:51:40 +0200 Subject: [PATCH 03/40] Favor final fields declared one per line Final fields should be favored wherever possible to advertise and enforce immutability. Also as a matter of style, favor declaring fields one per line instead of multiple on the same line. This style guideline is not consistent throughout the codebase, but is favored because it makes diffs / patches cleaner and is the more widely accepted style througout most modern Java codebases. --- .../core/grpc/AuthenticationInterceptor.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java index a4008583786..2e16491729c 100644 --- a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java @@ -6,8 +6,6 @@ import io.grpc.ServerInterceptor; import io.grpc.StatusRuntimeException; -import java.util.function.Predicate; - import lombok.extern.slf4j.Slf4j; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; @@ -20,7 +18,8 @@ @Slf4j public class AuthenticationInterceptor implements ServerInterceptor { - private String rpcUser, rpcPassword; + private final String rpcUser; + private final String rpcPassword; public AuthenticationInterceptor(String rpcUser, String rpcPassword) { this.rpcUser = rpcUser; @@ -42,7 +41,7 @@ private void authenticate(Metadata metadata) { throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Authentication token is missing")); } else { try { - if (isValidToken.test(authToken)) { + if (isValidToken(authToken)) { log.info("Authenticated user {} with token {}", rpcUser, authToken); } else { throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid username or password")); @@ -53,10 +52,8 @@ private void authenticate(Metadata metadata) { } } - private final Predicate isValidUser = (u) -> u.equals(rpcUser); - private final Predicate isValidPassword = (p) -> p.equals(rpcPassword); - private final Predicate isValidToken = (t) -> { - String[] pair = t.split(":"); - return isValidUser.test(pair[0]) && isValidPassword.test(pair[1]); - }; + private boolean isValidToken(String token) { + String[] pair = token.split(":"); + return pair[0].equals(rpcUser) && pair[1].equals(rpcPassword); + } } From 6490e97df231ca3877d81ecd1c5356e2d6b04982 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 10:03:36 +0200 Subject: [PATCH 04/40] Do not declare local variables as final Per https://github.com/bisq-network/style/issues/11 --- .../src/main/java/bisq/core/grpc/AuthenticationInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java index 2e16491729c..5aed895e399 100644 --- a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java @@ -36,7 +36,7 @@ public ServerCall.Listener interceptCall( } private void authenticate(Metadata metadata) { - final String authToken = metadata.get(Key.of("bisqd-creds", ASCII_STRING_MARSHALLER)); + String authToken = metadata.get(Key.of("bisqd-creds", ASCII_STRING_MARSHALLER)); if (authToken == null) { throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Authentication token is missing")); } else { From 864bd9a21a5cff168528e7a4eb5e4db984f69bf1 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 10:09:06 +0200 Subject: [PATCH 05/40] Wrap method parameters according to convention --- .../main/java/bisq/core/grpc/AuthenticationInterceptor.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java index 5aed895e399..eed8b823598 100644 --- a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java @@ -27,10 +27,8 @@ public AuthenticationInterceptor(String rpcUser, String rpcPassword) { } @Override - public ServerCall.Listener interceptCall( - ServerCall serverCall, - Metadata metadata, - ServerCallHandler serverCallHandler) { + public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, + ServerCallHandler serverCallHandler) { authenticate(metadata); return serverCallHandler.startCall(serverCall, metadata); } From bc88080df1164afa77310b9f78e2390bb18cbdc2 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 10:18:29 +0200 Subject: [PATCH 06/40] Simplify implementation of authenticate method --- .../core/grpc/AuthenticationInterceptor.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java index eed8b823598..d3b6d91a6c2 100644 --- a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java @@ -29,25 +29,18 @@ public AuthenticationInterceptor(String rpcUser, String rpcPassword) { @Override public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, ServerCallHandler serverCallHandler) { - authenticate(metadata); + authenticate(metadata.get(Key.of("bisqd-creds", ASCII_STRING_MARSHALLER))); return serverCallHandler.startCall(serverCall, metadata); } - private void authenticate(Metadata metadata) { - String authToken = metadata.get(Key.of("bisqd-creds", ASCII_STRING_MARSHALLER)); - if (authToken == null) { + private void authenticate(String authToken) { + if (authToken == null) throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Authentication token is missing")); - } else { - try { - if (isValidToken(authToken)) { - log.info("Authenticated user {} with token {}", rpcUser, authToken); - } else { - throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid username or password")); - } - } catch (Exception e) { - throw new StatusRuntimeException(UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e)); - } - } + + if (!isValidToken(authToken)) + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid username or password")); + + log.info("Authenticated user {} with token {}", rpcUser, authToken); } private boolean isValidToken(String token) { From 24c245c2ea1f9281cdf132341b5556c05318377e Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 10:26:44 +0200 Subject: [PATCH 07/40] Polish BisqCallCredentials style See https://github.com/bisq-network/style/issues/12 --- .../bisq/cli/app/BisqCallCredentials.java | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java index 1ddf89f9be9..d35caf1e78d 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java +++ b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java @@ -24,32 +24,27 @@ public BisqCallCredentials(Map credentials) { } @Override - public void applyRequestMetadata( - RequestInfo requestInfo, - Executor appExecutor, - MetadataApplier metadataApplier) { - + public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) { appExecutor.execute(() -> { try { Metadata headers = new Metadata(); Key creds = Key.of("bisqd-creds", ASCII_STRING_MARSHALLER); headers.put(creds, encodeCredentials()); metadataApplier.apply(headers); - } catch (Throwable e) { - metadataApplier.fail(UNAUTHENTICATED.withCause(e)); + } catch (Throwable ex) { + metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); } }); } - @Override - public void thisUsesUnstableApi() { - } - private String encodeCredentials() { - if (!credentials.containsKey(RPC_USER) || !credentials.containsKey(RPC_PASSWORD)) { + if (!credentials.containsKey(RPC_USER) || !credentials.containsKey(RPC_PASSWORD)) throw new ConfigException("Cannot call rpc service without username:password credentials"); - } else { - return credentials.get(RPC_USER) + ":" + credentials.get(RPC_PASSWORD); - } + + return credentials.get(RPC_USER) + ":" + credentials.get(RPC_PASSWORD); + } + + @Override + public void thisUsesUnstableApi() { } } From ca0658229b29786b5880ad7fb29164eeb6c9fd04 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 10:35:04 +0200 Subject: [PATCH 08/40] Rename AuthenticationInterceptor => TokenAuthInterceptor --- cli/src/main/java/bisq/cli/app/BisqCallCredentials.java | 2 +- core/src/main/java/bisq/core/grpc/BisqGrpcServer.java | 2 +- ...enticationInterceptor.java => TokenAuthInterceptor.java} | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename core/src/main/java/bisq/core/grpc/{AuthenticationInterceptor.java => TokenAuthInterceptor.java} (87%) diff --git a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java index d35caf1e78d..b3e92079c4b 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java +++ b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java @@ -28,7 +28,7 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, appExecutor.execute(() -> { try { Metadata headers = new Metadata(); - Key creds = Key.of("bisqd-creds", ASCII_STRING_MARSHALLER); + Key creds = Key.of("bisq-api-token", ASCII_STRING_MARSHALLER); headers.put(creds, encodeCredentials()); metadataApplier.apply(headers); } catch (Throwable ex) { diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java index aeee2cf9e2a..df185fa35cc 100644 --- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -217,7 +217,7 @@ private void start() throws IOException { .addService(new GetPaymentAccountsImpl()) .addService(new PlaceOfferImpl()) .addService(new StopServerImpl()) - .intercept(new AuthenticationInterceptor(rpcUser, rpcPassword)) + .intercept(new TokenAuthInterceptor(rpcUser, rpcPassword)) .build() .start(); diff --git a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java b/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java similarity index 87% rename from core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java rename to core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java index d3b6d91a6c2..13733ef6f42 100644 --- a/core/src/main/java/bisq/core/grpc/AuthenticationInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java @@ -16,12 +16,12 @@ * Simple authentication interceptor to validate a cleartext token in username:password format. */ @Slf4j -public class AuthenticationInterceptor implements ServerInterceptor { +public class TokenAuthInterceptor implements ServerInterceptor { private final String rpcUser; private final String rpcPassword; - public AuthenticationInterceptor(String rpcUser, String rpcPassword) { + public TokenAuthInterceptor(String rpcUser, String rpcPassword) { this.rpcUser = rpcUser; this.rpcPassword = rpcPassword; } @@ -29,7 +29,7 @@ public AuthenticationInterceptor(String rpcUser, String rpcPassword) { @Override public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, ServerCallHandler serverCallHandler) { - authenticate(metadata.get(Key.of("bisqd-creds", ASCII_STRING_MARSHALLER))); + authenticate(metadata.get(Key.of("bisq-api-token", ASCII_STRING_MARSHALLER))); return serverCallHandler.startCall(serverCall, metadata); } From 1a133f4b67f29a16fe654b1915dfec509eb7f8f5 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 11:32:19 +0200 Subject: [PATCH 09/40] Use a single auth token vs username:password There is actually no use case for both username and password forming the authentication token. This change simplifies the arrangement such that a single token is passed. --rpcUser and --rpcPassword are no longer used and are replaced by --apiToken on both cli and daemon sides. --- .../bisq/cli/app/BisqCallCredentials.java | 20 +++-------- cli/src/main/java/bisq/cli/app/CliConfig.java | 23 +++++-------- .../main/java/bisq/cli/app/CommandParser.java | 34 +++---------------- .../main/java/bisq/cli/app/RpcCommand.java | 2 +- .../main/java/bisq/common/config/Config.java | 8 +++++ .../java/bisq/core/grpc/BisqGrpcServer.java | 4 +-- .../bisq/core/grpc/TokenAuthInterceptor.java | 21 ++++-------- 7 files changed, 34 insertions(+), 78 deletions(-) diff --git a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java index b3e92079c4b..2f8af1e00cc 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java +++ b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java @@ -4,11 +4,8 @@ import io.grpc.Metadata; import io.grpc.Metadata.Key; -import java.util.Map; import java.util.concurrent.Executor; -import static bisq.cli.app.CliConfig.RPC_PASSWORD; -import static bisq.cli.app.CliConfig.RPC_USER; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static io.grpc.Status.UNAUTHENTICATED; @@ -17,10 +14,10 @@ */ public class BisqCallCredentials extends CallCredentials { - private final Map credentials; + private final String apiToken; - public BisqCallCredentials(Map credentials) { - this.credentials = credentials; + public BisqCallCredentials(String apiToken) { + this.apiToken = apiToken; } @Override @@ -28,8 +25,8 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, appExecutor.execute(() -> { try { Metadata headers = new Metadata(); - Key creds = Key.of("bisq-api-token", ASCII_STRING_MARSHALLER); - headers.put(creds, encodeCredentials()); + Key apiTokenKey = Key.of("bisq-api-token", ASCII_STRING_MARSHALLER); + headers.put(apiTokenKey, apiToken); metadataApplier.apply(headers); } catch (Throwable ex) { metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); @@ -37,13 +34,6 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, }); } - private String encodeCredentials() { - if (!credentials.containsKey(RPC_USER) || !credentials.containsKey(RPC_PASSWORD)) - throw new ConfigException("Cannot call rpc service without username:password credentials"); - - return credentials.get(RPC_USER) + ":" + credentials.get(RPC_PASSWORD); - } - @Override public void thisUsesUnstableApi() { } diff --git a/cli/src/main/java/bisq/cli/app/CliConfig.java b/cli/src/main/java/bisq/cli/app/CliConfig.java index dbe2d1af333..57c90418b2c 100644 --- a/cli/src/main/java/bisq/cli/app/CliConfig.java +++ b/cli/src/main/java/bisq/cli/app/CliConfig.java @@ -16,12 +16,10 @@ final class CliConfig { static final String STOPSERVER = "stopserver"; // Argument accepting name constants - static final String RPC_USER = "rpcuser"; - static final String RPC_PASSWORD = "rpcpassword"; + static final String API_TOKEN = "apiToken"; // Argument accepting cmd options - final String rpcUser; - final String rpcPassword; + final String apiToken; // The parser that will be used to parse both cmd line and config file options private final OptionParser optionParser = new OptionParser(); @@ -30,22 +28,17 @@ final class CliConfig { CliConfig(String[] params) { this.params = params; - ArgumentAcceptingOptionSpec rpcUserOpt = - optionParser.accepts(RPC_USER, "Bisq daemon username") - .withRequiredArg() - .defaultsTo(""); - ArgumentAcceptingOptionSpec rpcPasswordOpt = - optionParser.accepts(RPC_PASSWORD, "Bisq daemon password") - .withRequiredArg() - .defaultsTo(""); + ArgumentAcceptingOptionSpec apiTokenOpt = + optionParser.accepts(API_TOKEN, "Bisq API token") + .withRequiredArg(); + try { CompositeOptionSet options = new CompositeOptionSet(); // Parse command line options OptionSet cliOpts = optionParser.parse(params); options.addOptionSet(cliOpts); - this.rpcUser = options.valueOf(rpcUserOpt); - this.rpcPassword = options.valueOf(rpcPasswordOpt); + this.apiToken = options.valueOf(apiTokenOpt); optionParser.allowsUnrecognizedOptions(); optionParser.nonOptions(GETBALANCE).ofType(String.class).describedAs("Get btc balance"); @@ -69,6 +62,6 @@ public String[] getParams() { } static void printHelp() { - out.println("Usage: bisq-cli --rpcpassword=user --rpcpassword=password getbalance | getversion"); + out.println("Usage: bisq-cli --apiToken=token getbalance | getversion"); } } diff --git a/cli/src/main/java/bisq/cli/app/CommandParser.java b/cli/src/main/java/bisq/cli/app/CommandParser.java index 827dd4d4041..e51cc1578c7 100644 --- a/cli/src/main/java/bisq/cli/app/CommandParser.java +++ b/cli/src/main/java/bisq/cli/app/CommandParser.java @@ -4,19 +4,13 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static java.lang.System.out; final class CommandParser { private Optional cmdToken; - private final Map creds = new HashMap<>(); + private String apiToken; private final CliConfig config; public CommandParser(CliConfig config) { @@ -25,37 +19,19 @@ public CommandParser(CliConfig config) { } public Optional getCmdToken() { - return this.cmdToken; + return cmdToken; } - public Map getCreds() { - return this.creds; + public String getApiToken() { + return apiToken; } private void init() { OptionParser parser = config.getOptionParser(); OptionSpec nonOptions = parser.nonOptions().ofType(String.class); OptionSet options = parser.parse(config.getParams()); - creds.putAll(rpcCredentials.apply(options)); - // debugOptionsSet(options, nonOptions); + apiToken = (String) options.valueOf("apiToken"); List detectedOptions = nonOptions.values(options); cmdToken = detectedOptions.isEmpty() ? Optional.empty() : Optional.of(detectedOptions.get(0)); } - - final Function> rpcCredentials = (opts) -> - opts.asMap().entrySet().stream() - .filter(e -> e.getKey().options().size() == 1 && e.getKey().options().get(0).startsWith("rpc")) - .collect(Collectors.toUnmodifiableMap(m -> m.getKey().options().get(0), m -> (String) m.getValue().get(0))); - - private void debugOptionsSet(OptionSet options, OptionSpec nonOptions) { - // https://programtalk.com/java-api-usage-examples/joptsimple.OptionParser - out.println("*** BEGIN Debug OptionSet ***"); - out.println("[argument acceptors]"); - options.asMap().entrySet().forEach(out::println); - out.println("[rpc credentials map]"); - out.println(rpcCredentials.apply(options)); - out.println("[non options]"); - nonOptions.values(options).forEach(out::println); - out.println("*** END Debug OptionSet ***"); - } } diff --git a/cli/src/main/java/bisq/cli/app/RpcCommand.java b/cli/src/main/java/bisq/cli/app/RpcCommand.java index b8455e7711f..06dc5868298 100644 --- a/cli/src/main/java/bisq/cli/app/RpcCommand.java +++ b/cli/src/main/java/bisq/cli/app/RpcCommand.java @@ -32,7 +32,7 @@ final class RpcCommand { final Function prettyBalance = (sats) -> btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor)); RpcCommand(ManagedChannel channel, CommandParser parser) { - this.callCredentials = new BisqCallCredentials(parser.getCreds()); + this.callCredentials = new BisqCallCredentials(parser.getApiToken()); this.getBalanceStub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); this.getVersionStub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); this.stopServerStub = StopServerGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index ddefb748bab..0bb6481276c 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_TOKEN = "apiToken"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -199,6 +200,7 @@ public class Config { public final long genesisTotalSupply; public final boolean dumpDelayedPayoutTxs; public final boolean allowFaultyDelayedTxs; + public final String apiToken; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -615,6 +617,11 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) { .ofType(boolean.class) .defaultsTo(false); + ArgumentAcceptingOptionSpec apiTokenOpt = + parser.accepts(API_TOKEN, "Bisq gRPC API authentication token") + .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.apiToken = options.valueOf(apiTokenOpt); } 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 df185fa35cc..066e36dfd42 100644 --- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -205,8 +205,6 @@ public void stop() { private void start() throws IOException { // TODO add to options int port = 9998; - String rpcUser = config.rpcUser; - String rpcPassword = config.rpcPassword; // Config services server = ServerBuilder.forPort(port) @@ -217,7 +215,7 @@ private void start() throws IOException { .addService(new GetPaymentAccountsImpl()) .addService(new PlaceOfferImpl()) .addService(new StopServerImpl()) - .intercept(new TokenAuthInterceptor(rpcUser, rpcPassword)) + .intercept(new TokenAuthInterceptor(config.apiToken)) .build() .start(); diff --git a/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java b/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java index 13733ef6f42..11aec6a10e1 100644 --- a/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java @@ -18,12 +18,10 @@ @Slf4j public class TokenAuthInterceptor implements ServerInterceptor { - private final String rpcUser; - private final String rpcPassword; + private final String apiToken; - public TokenAuthInterceptor(String rpcUser, String rpcPassword) { - this.rpcUser = rpcUser; - this.rpcPassword = rpcPassword; + public TokenAuthInterceptor(String apiToken) { + this.apiToken = apiToken; } @Override @@ -35,16 +33,9 @@ public ServerCall.Listener interceptCall(ServerCall Date: Thu, 23 Apr 2020 13:53:14 +0200 Subject: [PATCH 10/40] Simplify implementation to a single main class --- .../main/java/bisq/cli/app/BisqCliMain.java | 165 +++++++++++------- .../main/java/bisq/cli/app/BisqException.java | 16 -- cli/src/main/java/bisq/cli/app/CliConfig.java | 67 ------- .../main/java/bisq/cli/app/CommandParser.java | 37 ---- .../java/bisq/cli/app/CompositeOptionSet.java | 56 ------ .../java/bisq/cli/app/ConfigException.java | 9 - .../main/java/bisq/cli/app/RpcCommand.java | 68 -------- 7 files changed, 106 insertions(+), 312 deletions(-) delete mode 100644 cli/src/main/java/bisq/cli/app/BisqException.java delete mode 100644 cli/src/main/java/bisq/cli/app/CliConfig.java delete mode 100644 cli/src/main/java/bisq/cli/app/CommandParser.java delete mode 100644 cli/src/main/java/bisq/cli/app/CompositeOptionSet.java delete mode 100644 cli/src/main/java/bisq/cli/app/ConfigException.java delete mode 100644 cli/src/main/java/bisq/cli/app/RpcCommand.java diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java index 89be7adb0d0..abbeb374d7e 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -17,23 +17,36 @@ 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 io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import joptsimple.AbstractOptionSpec; +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import java.text.DecimalFormat; + +import java.io.IOException; + +import java.math.BigDecimal; + +import java.util.List; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; -import static bisq.cli.app.CliConfig.GETBALANCE; -import static bisq.cli.app.CliConfig.GETVERSION; -import static bisq.cli.app.CliConfig.HELP; -import static bisq.cli.app.CliConfig.STOPSERVER; -import static java.lang.String.format; +import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; /** - * gRPC client. + * A command-line client for the Bisq gRPC API */ @Slf4j public class BisqCliMain { @@ -41,67 +54,101 @@ public class BisqCliMain { private static final int EXIT_SUCCESS = 0; private static final int EXIT_FAILURE = 1; - private final ManagedChannel channel; - private final RpcCommand rpcCommand; - private final CliConfig config; - private final CommandParser parser; + public static void main(String[] args) throws IOException { + OptionParser parser = new OptionParser(); - public static void main(String[] args) { - new BisqCliMain("localhost", 9998, args); - } + AbstractOptionSpec helpOpt = + parser.accepts("help", "Print this help text") + .forHelp(); - private BisqCliMain(String host, int port, String[] args) { - this(ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(), args); - String result = runCommand(); - out.println(result); - try { - shutdown(); // Orderly channel shutdown - } catch (InterruptedException ignored) { + ArgumentAcceptingOptionSpec hostOpt = + parser.accepts("host", "Bisq node hostname or IP") + .withRequiredArg() + .defaultsTo("localhost"); + + ArgumentAcceptingOptionSpec portOpt = + parser.accepts("port", "Bisq node RPC port") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9998); + + ArgumentAcceptingOptionSpec authOpt = + parser.accepts("auth", "Bisq node RPC authentication token") + .withRequiredArg(); + + OptionSet options = parser.parse(args); + + if (options.has(helpOpt)) { + out.println("Bisq RPC Client v0.1.0"); + out.println(); + out.println("Usage: bisq-cli [options] "); + out.println(); + parser.printHelpOn(out); + out.println(); + out.println("Command Descripiton"); + out.println("------- -----------"); + out.println("getversion Get Bisq node version"); + out.println("getbalance Get Bisq node wallet balance"); + out.println(); + exit(EXIT_SUCCESS); } - } - /** - * Construct client for accessing server using the existing channel. - */ - private BisqCliMain(ManagedChannel channel, String[] args) { - this.channel = channel; - this.config = new CliConfig(args); - this.parser = new CommandParser(config); - this.rpcCommand = new RpcCommand(channel, parser); - } + String host = options.valueOf(hostOpt); + int port = options.valueOf(portOpt); - private String runCommand() { - if (parser.getCmdToken().isPresent()) { - final String cmdToken = parser.getCmdToken().get(); - final String result; - switch (cmdToken) { - case HELP: - CliConfig.printHelp(); - exit(EXIT_SUCCESS); - case GETBALANCE: - long satoshis = rpcCommand.getBalance(); - result = satoshis == -1 ? "Server initializing..." : rpcCommand.prettyBalance.apply(satoshis); - break; - case GETVERSION: - result = rpcCommand.getVersion(); - break; - case STOPSERVER: - rpcCommand.stopServer(); - result = "Server stopped"; - break; - default: - result = format("Unknown command '%s'", cmdToken); - } - return result; - } else { - CliConfig.printHelp(); + String authToken = options.valueOf(authOpt); + if (authToken == null) { + err.println("error: Authentication token must not be null"); exit(EXIT_FAILURE); - return null; } + + @SuppressWarnings("unchecked") + List nonOptionArgs = (List) options.nonOptionArguments(); + if (nonOptionArgs.isEmpty()) { + err.println("error: No RPC command specified"); + exit(EXIT_FAILURE); + } + + ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); + BisqCallCredentials credentials = new BisqCallCredentials(authToken); + + String command = nonOptionArgs.get(0); + + if ("getversion".equals(command)) { + GetVersionRequest request = GetVersionRequest.newBuilder().build(); + GetVersionGrpc.GetVersionBlockingStub getVersionStub = + GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + out.println(getVersionStub.getVersion(request).getVersion()); + shutdown(channel); + exit(EXIT_SUCCESS); + } + + if ("getbalance".equals(command)) { + GetBalanceGrpc.GetBalanceBlockingStub getBalanceStub = + GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials); + GetBalanceRequest request = GetBalanceRequest.newBuilder().build(); + long satoshis = getBalanceStub.getBalance(request).getBalance(); + out.println(satoshis == -1 ? "Server initializing..." : formatSatoshis(satoshis)); + shutdown(channel); + exit(EXIT_SUCCESS); + } + + err.printf("error: unknown rpc command '%s'\n", command); + exit(EXIT_FAILURE); + } + + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + private static String formatSatoshis(long satoshis) { + DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000"); + BigDecimal satoshiDivisor = new BigDecimal(100000000); + return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor)); } - private void shutdown() throws InterruptedException { - channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); - exit(EXIT_SUCCESS); + private static void shutdown(ManagedChannel channel) { + try { + channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } } } diff --git a/cli/src/main/java/bisq/cli/app/BisqException.java b/cli/src/main/java/bisq/cli/app/BisqException.java deleted file mode 100644 index 0248a9d9b38..00000000000 --- a/cli/src/main/java/bisq/cli/app/BisqException.java +++ /dev/null @@ -1,16 +0,0 @@ -package bisq.cli.app; - -public class BisqException extends RuntimeException { - - public BisqException(Throwable cause) { - super(cause); - } - - public BisqException(String format, Object... args) { - super(String.format(format, args)); - } - - public BisqException(Throwable cause, String format, Object... args) { - super(String.format(format, args), cause); - } -} diff --git a/cli/src/main/java/bisq/cli/app/CliConfig.java b/cli/src/main/java/bisq/cli/app/CliConfig.java deleted file mode 100644 index 57c90418b2c..00000000000 --- a/cli/src/main/java/bisq/cli/app/CliConfig.java +++ /dev/null @@ -1,67 +0,0 @@ -package bisq.cli.app; - -import joptsimple.ArgumentAcceptingOptionSpec; -import joptsimple.OptionException; -import joptsimple.OptionParser; -import joptsimple.OptionSet; - -import static java.lang.System.out; - -final class CliConfig { - - // Non-Option argument name constants - static final String HELP = "help"; - static final String GETBALANCE = "getbalance"; - static final String GETVERSION = "getversion"; - static final String STOPSERVER = "stopserver"; - - // Argument accepting name constants - static final String API_TOKEN = "apiToken"; - - // Argument accepting cmd options - final String apiToken; - - // The parser that will be used to parse both cmd line and config file options - private final OptionParser optionParser = new OptionParser(); - private final String[] params; - - CliConfig(String[] params) { - this.params = params; - - ArgumentAcceptingOptionSpec apiTokenOpt = - optionParser.accepts(API_TOKEN, "Bisq API token") - .withRequiredArg(); - - try { - CompositeOptionSet options = new CompositeOptionSet(); - // Parse command line options - OptionSet cliOpts = optionParser.parse(params); - options.addOptionSet(cliOpts); - - this.apiToken = options.valueOf(apiTokenOpt); - - optionParser.allowsUnrecognizedOptions(); - optionParser.nonOptions(GETBALANCE).ofType(String.class).describedAs("Get btc balance"); - optionParser.nonOptions(GETVERSION).ofType(String.class).describedAs("Get bisq version"); - - } catch (OptionException ex) { - throw new ConfigException("Problem parsing option '%s': %s", - ex.options().get(0), - ex.getCause() != null ? - ex.getCause().getMessage() : - ex.getMessage()); - } - } - - OptionParser getOptionParser() { - return this.optionParser; - } - - public String[] getParams() { - return this.params; - } - - static void printHelp() { - out.println("Usage: bisq-cli --apiToken=token getbalance | getversion"); - } -} 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 e51cc1578c7..00000000000 --- a/cli/src/main/java/bisq/cli/app/CommandParser.java +++ /dev/null @@ -1,37 +0,0 @@ -package bisq.cli.app; - -import joptsimple.OptionParser; -import joptsimple.OptionSet; -import joptsimple.OptionSpec; - -import java.util.List; -import java.util.Optional; - -final class CommandParser { - - private Optional cmdToken; - private String apiToken; - private final CliConfig config; - - public CommandParser(CliConfig config) { - this.config = config; - init(); - } - - public Optional getCmdToken() { - return cmdToken; - } - - public String getApiToken() { - return apiToken; - } - - private void init() { - OptionParser parser = config.getOptionParser(); - OptionSpec nonOptions = parser.nonOptions().ofType(String.class); - OptionSet options = parser.parse(config.getParams()); - apiToken = (String) options.valueOf("apiToken"); - List detectedOptions = nonOptions.values(options); - cmdToken = detectedOptions.isEmpty() ? Optional.empty() : Optional.of(detectedOptions.get(0)); - } -} diff --git a/cli/src/main/java/bisq/cli/app/CompositeOptionSet.java b/cli/src/main/java/bisq/cli/app/CompositeOptionSet.java deleted file mode 100644 index ffea7fe01b3..00000000000 --- a/cli/src/main/java/bisq/cli/app/CompositeOptionSet.java +++ /dev/null @@ -1,56 +0,0 @@ -package bisq.cli.app; - - -import joptsimple.ArgumentAcceptingOptionSpec; -import joptsimple.OptionSet; -import joptsimple.OptionSpec; - -import java.util.ArrayList; -import java.util.List; - -/** - * Composes multiple JOptSimple {@link OptionSet} instances such that calls to - * {@link #valueOf(OptionSpec)} and co will search all instances in the order they were - * added and return any value explicitly set, otherwise returning the default value for - * the given option or null if no default has been set. The API found here loosely - * emulates the {@link OptionSet} API without going through the unnecessary work of - * actually extending it. In practice, this class is used to compose options provided at - * the command line with those provided via config file, such that those provided at the - * command line take precedence over those provided in the config file. - */ -class CompositeOptionSet { - - private final List optionSets = new ArrayList<>(); - - public void addOptionSet(OptionSet optionSet) { - optionSets.add(optionSet); - } - - public boolean has(OptionSpec option) { - for (OptionSet optionSet : optionSets) - if (optionSet.has(option)) - return true; - - return false; - } - - public V valueOf(OptionSpec option) { - for (OptionSet optionSet : optionSets) - if (optionSet.has(option)) - return optionSet.valueOf(option); - - // None of the provided option sets specified the given option so fall back to - // the default value (if any) provided by the first specified OptionSet - return optionSets.get(0).valueOf(option); - } - - public List valuesOf(ArgumentAcceptingOptionSpec option) { - for (OptionSet optionSet : optionSets) - if (optionSet.has(option)) - return optionSet.valuesOf(option); - - // None of the provided option sets specified the given option so fall back to - // the default value (if any) provided by the first specified OptionSet - return optionSets.get(0).valuesOf(option); - } -} diff --git a/cli/src/main/java/bisq/cli/app/ConfigException.java b/cli/src/main/java/bisq/cli/app/ConfigException.java deleted file mode 100644 index 3ea785f2b16..00000000000 --- a/cli/src/main/java/bisq/cli/app/ConfigException.java +++ /dev/null @@ -1,9 +0,0 @@ -package bisq.cli.app; - -public class ConfigException extends BisqException { - - public ConfigException(String format, Object... args) { - super(format, args); - } - -} diff --git a/cli/src/main/java/bisq/cli/app/RpcCommand.java b/cli/src/main/java/bisq/cli/app/RpcCommand.java deleted file mode 100644 index 06dc5868298..00000000000 --- a/cli/src/main/java/bisq/cli/app/RpcCommand.java +++ /dev/null @@ -1,68 +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 RpcCommand { - - private final BisqCallCredentials callCredentials; - 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)); - - RpcCommand(ManagedChannel channel, CommandParser parser) { - this.callCredentials = new BisqCallCredentials(parser.getApiToken()); - this.getBalanceStub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); - this.getVersionStub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); - this.stopServerStub = StopServerGrpc.newBlockingStub(channel).withCallCredentials(callCredentials); - } - - 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()); - } - } -} From 04defcb66df7292e9975bf5cf1d00a26f844ae08 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 23 Apr 2020 17:36:47 +0200 Subject: [PATCH 11/40] Update comments and console output --- cli/src/main/java/bisq/cli/app/BisqCliMain.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java index abbeb374d7e..5716eaa4baa 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -46,7 +46,7 @@ import static java.lang.System.out; /** - * A command-line client for the Bisq gRPC API + * A command-line client for the Bisq gRPC API. */ @Slf4j public class BisqCliMain { @@ -79,7 +79,7 @@ public static void main(String[] args) throws IOException { OptionSet options = parser.parse(args); if (options.has(helpOpt)) { - out.println("Bisq RPC Client v0.1.0"); + out.println("Bisq RPC Client"); out.println(); out.println("Usage: bisq-cli [options] "); out.println(); @@ -98,14 +98,14 @@ public static void main(String[] args) throws IOException { String authToken = options.valueOf(authOpt); if (authToken == null) { - err.println("error: Authentication token must not be null"); + err.println("error: rpc authentication token must not be null"); exit(EXIT_FAILURE); } @SuppressWarnings("unchecked") List nonOptionArgs = (List) options.nonOptionArguments(); if (nonOptionArgs.isEmpty()) { - err.println("error: No RPC command specified"); + err.println("error: no rpc command specified"); exit(EXIT_FAILURE); } @@ -128,7 +128,12 @@ public static void main(String[] args) throws IOException { GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials); GetBalanceRequest request = GetBalanceRequest.newBuilder().build(); long satoshis = getBalanceStub.getBalance(request).getBalance(); - out.println(satoshis == -1 ? "Server initializing..." : formatSatoshis(satoshis)); + if (satoshis == -1) { + err.println("Server initializing..."); + shutdown(channel); + exit(EXIT_FAILURE); + } + out.println(formatBalance(satoshis)); shutdown(channel); exit(EXIT_SUCCESS); } @@ -138,7 +143,7 @@ public static void main(String[] args) throws IOException { } @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") - private static String formatSatoshis(long satoshis) { + private static String formatBalance(long satoshis) { DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000"); BigDecimal satoshiDivisor = new BigDecimal(100000000); return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor)); From 3fe7848c4ee9bcb25eb18ae070fca786270dc41c Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 08:14:23 +0200 Subject: [PATCH 12/40] Use var declarations where appropriate --- .../main/java/bisq/cli/app/BisqCliMain.java | 70 ++++++++----------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java index 5716eaa4baa..dbd507a736a 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -25,10 +25,7 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; -import joptsimple.AbstractOptionSpec; -import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionParser; -import joptsimple.OptionSet; import java.text.DecimalFormat; @@ -55,28 +52,24 @@ public class BisqCliMain { private static final int EXIT_FAILURE = 1; public static void main(String[] args) throws IOException { - OptionParser parser = new OptionParser(); + var parser = new OptionParser(); - AbstractOptionSpec helpOpt = - parser.accepts("help", "Print this help text") - .forHelp(); + var helpOpt = parser.accepts("help", "Print this help text") + .forHelp(); - ArgumentAcceptingOptionSpec hostOpt = - parser.accepts("host", "Bisq node hostname or IP") - .withRequiredArg() - .defaultsTo("localhost"); + var hostOpt = parser.accepts("host", "Bisq node hostname or IP") + .withRequiredArg() + .defaultsTo("localhost"); - ArgumentAcceptingOptionSpec portOpt = - parser.accepts("port", "Bisq node RPC port") - .withRequiredArg() - .ofType(Integer.class) - .defaultsTo(9998); + var portOpt = parser.accepts("port", "Bisq node RPC port") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(9998); - ArgumentAcceptingOptionSpec authOpt = - parser.accepts("auth", "Bisq node RPC authentication token") - .withRequiredArg(); + var authOpt = parser.accepts("auth", "Bisq node RPC authentication token") + .withRequiredArg(); - OptionSet options = parser.parse(args); + var options = parser.parse(args); if (options.has(helpOpt)) { out.println("Bisq RPC Client"); @@ -93,47 +86,46 @@ public static void main(String[] args) throws IOException { exit(EXIT_SUCCESS); } - String host = options.valueOf(hostOpt); - int port = options.valueOf(portOpt); + var host = options.valueOf(hostOpt); + var port = options.valueOf(portOpt); - String authToken = options.valueOf(authOpt); + var authToken = options.valueOf(authOpt); if (authToken == null) { err.println("error: rpc authentication token must not be null"); exit(EXIT_FAILURE); } @SuppressWarnings("unchecked") - List nonOptionArgs = (List) options.nonOptionArguments(); + var nonOptionArgs = (List) options.nonOptionArguments(); if (nonOptionArgs.isEmpty()) { err.println("error: no rpc command specified"); exit(EXIT_FAILURE); } - ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - BisqCallCredentials credentials = new BisqCallCredentials(authToken); + var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); + var credentials = new BisqCallCredentials(authToken); - String command = nonOptionArgs.get(0); + var command = nonOptionArgs.get(0); if ("getversion".equals(command)) { - GetVersionRequest request = GetVersionRequest.newBuilder().build(); - GetVersionGrpc.GetVersionBlockingStub getVersionStub = - GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - out.println(getVersionStub.getVersion(request).getVersion()); + var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetVersionRequest.newBuilder().build(); + var version = stub.getVersion(request).getVersion(); + out.println(version); shutdown(channel); exit(EXIT_SUCCESS); } if ("getbalance".equals(command)) { - GetBalanceGrpc.GetBalanceBlockingStub getBalanceStub = - GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials); - GetBalanceRequest request = GetBalanceRequest.newBuilder().build(); - long satoshis = getBalanceStub.getBalance(request).getBalance(); - if (satoshis == -1) { + var stub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetBalanceRequest.newBuilder().build(); + var balance = stub.getBalance(request).getBalance(); + if (balance == -1) { err.println("Server initializing..."); shutdown(channel); exit(EXIT_FAILURE); } - out.println(formatBalance(satoshis)); + out.println(formatBalance(balance)); shutdown(channel); exit(EXIT_SUCCESS); } @@ -144,8 +136,8 @@ public static void main(String[] args) throws IOException { @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") private static String formatBalance(long satoshis) { - DecimalFormat btcFormat = new DecimalFormat("###,##0.00000000"); - BigDecimal satoshiDivisor = new BigDecimal(100000000); + var btcFormat = new DecimalFormat("###,##0.00000000"); + var satoshiDivisor = new BigDecimal(100000000); return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor)); } From e84123c20a80ee2e50dc10a0e90cef06f13a7acb Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 09:37:14 +0200 Subject: [PATCH 13/40] Refactor auth infrastructure naming --- ...ls.java => AuthHeaderCallCredentials.java} | 18 +++---- .../main/java/bisq/cli/app/BisqCliMain.java | 12 ++--- .../main/java/bisq/common/config/Config.java | 10 ++-- .../core/grpc/AuthorizationInterceptor.java | 48 +++++++++++++++++++ .../java/bisq/core/grpc/BisqGrpcServer.java | 2 +- .../bisq/core/grpc/TokenAuthInterceptor.java | 41 ---------------- 6 files changed, 70 insertions(+), 61 deletions(-) rename cli/src/main/java/bisq/cli/app/{BisqCallCredentials.java => AuthHeaderCallCredentials.java} (55%) create mode 100644 core/src/main/java/bisq/core/grpc/AuthorizationInterceptor.java delete mode 100644 core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java diff --git a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java b/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java similarity index 55% rename from cli/src/main/java/bisq/cli/app/BisqCallCredentials.java rename to cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java index 2f8af1e00cc..d7f98e7269a 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java +++ b/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java @@ -10,23 +10,25 @@ import static io.grpc.Status.UNAUTHENTICATED; /** - * Simple credentials implementation for sending cleartext username:password token via rpc call headers. + * Sets the {@value AUTH_HEADER_KEY} rpc call header to a given value. */ -public class BisqCallCredentials extends CallCredentials { +public class AuthHeaderCallCredentials extends CallCredentials { - private final String apiToken; + public static final String AUTH_HEADER_KEY = "authorization"; - public BisqCallCredentials(String apiToken) { - this.apiToken = apiToken; + private final String authHeaderValue; + + public AuthHeaderCallCredentials(String authHeaderValue) { + this.authHeaderValue = authHeaderValue; } @Override public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier metadataApplier) { appExecutor.execute(() -> { try { - Metadata headers = new Metadata(); - Key apiTokenKey = Key.of("bisq-api-token", ASCII_STRING_MARSHALLER); - headers.put(apiTokenKey, apiToken); + var headers = new Metadata(); + var authorizationKey = Key.of(AUTH_HEADER_KEY, ASCII_STRING_MARSHALLER); + headers.put(authorizationKey, authHeaderValue); metadataApplier.apply(headers); } catch (Throwable ex) { metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/app/BisqCliMain.java index dbd507a736a..3e69b17b841 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/app/BisqCliMain.java @@ -61,12 +61,12 @@ public static void main(String[] args) throws IOException { .withRequiredArg() .defaultsTo("localhost"); - var portOpt = parser.accepts("port", "Bisq node RPC port") + var portOpt = parser.accepts("port", "Bisq node rpc port") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9998); - var authOpt = parser.accepts("auth", "Bisq node RPC authentication token") + var passwordOpt = parser.accepts("password", "Bisq node rpc server password") .withRequiredArg(); var options = parser.parse(args); @@ -89,9 +89,9 @@ public static void main(String[] args) throws IOException { var host = options.valueOf(hostOpt); var port = options.valueOf(portOpt); - var authToken = options.valueOf(authOpt); - if (authToken == null) { - err.println("error: rpc authentication token must not be null"); + var password = options.valueOf(passwordOpt); + if (password == null) { + err.println("error: rpc password must not be null"); exit(EXIT_FAILURE); } @@ -103,7 +103,7 @@ public static void main(String[] args) throws IOException { } var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - var credentials = new BisqCallCredentials(authToken); + var credentials = new AuthHeaderCallCredentials(password); var command = nonOptionArgs.get(0); diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index 0bb6481276c..b04c58e73ef 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -116,7 +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_TOKEN = "apiToken"; + public static final String API_PASSWORD = "apiPassword"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -200,7 +200,7 @@ public class Config { public final long genesisTotalSupply; public final boolean dumpDelayedPayoutTxs; public final boolean allowFaultyDelayedTxs; - public final String apiToken; + public final String apiPassword; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -617,8 +617,8 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) { .ofType(boolean.class) .defaultsTo(false); - ArgumentAcceptingOptionSpec apiTokenOpt = - parser.accepts(API_TOKEN, "Bisq gRPC API authentication token") + ArgumentAcceptingOptionSpec apiPasswordOpt = + parser.accepts(API_PASSWORD, "gRPC API password") .withRequiredArg() .defaultsTo(""); @@ -734,7 +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.apiToken = options.valueOf(apiTokenOpt); + 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/AuthorizationInterceptor.java b/core/src/main/java/bisq/core/grpc/AuthorizationInterceptor.java new file mode 100644 index 00000000000..de8baf92ae2 --- /dev/null +++ b/core/src/main/java/bisq/core/grpc/AuthorizationInterceptor.java @@ -0,0 +1,48 @@ +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 lombok.extern.slf4j.Slf4j; + +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 AUTH_HEADER_KEY} header to an expected value set at server startup time. + * + * @see bisq.common.config.Config#apiPassword + */ +@Slf4j +public class AuthorizationInterceptor implements ServerInterceptor { + + public static final String AUTH_HEADER_KEY = "authorization"; + + private final String expectedAuthHeaderValue; + + public AuthorizationInterceptor(String expectedAuthHeaderValue) { + this.expectedAuthHeaderValue = expectedAuthHeaderValue; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata headers, + ServerCallHandler serverCallHandler) { + var actualAuthHeaderValue = headers.get(Key.of(AUTH_HEADER_KEY, ASCII_STRING_MARSHALLER)); + + if (actualAuthHeaderValue == null) + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( + format("missing '%s' rpc header value", AUTH_HEADER_KEY))); + + if (!actualAuthHeaderValue.equals(expectedAuthHeaderValue)) + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( + format("incorrect '%s' rpc header value", AUTH_HEADER_KEY))); + + return serverCallHandler.startCall(serverCall, headers); + } +} diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java index 066e36dfd42..5c434831a8a 100644 --- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -215,7 +215,7 @@ private void start() throws IOException { .addService(new GetPaymentAccountsImpl()) .addService(new PlaceOfferImpl()) .addService(new StopServerImpl()) - .intercept(new TokenAuthInterceptor(config.apiToken)) + .intercept(new AuthorizationInterceptor(config.apiPassword)) .build() .start(); diff --git a/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java b/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java deleted file mode 100644 index 11aec6a10e1..00000000000 --- a/core/src/main/java/bisq/core/grpc/TokenAuthInterceptor.java +++ /dev/null @@ -1,41 +0,0 @@ -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 lombok.extern.slf4j.Slf4j; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static io.grpc.Metadata.Key; -import static io.grpc.Status.UNAUTHENTICATED; - -/** - * Simple authentication interceptor to validate a cleartext token in username:password format. - */ -@Slf4j -public class TokenAuthInterceptor implements ServerInterceptor { - - private final String apiToken; - - public TokenAuthInterceptor(String apiToken) { - this.apiToken = apiToken; - } - - @Override - public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, - ServerCallHandler serverCallHandler) { - authenticate(metadata.get(Key.of("bisq-api-token", ASCII_STRING_MARSHALLER))); - return serverCallHandler.startCall(serverCall, metadata); - } - - private void authenticate(String authToken) { - if (authToken == null) - throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("API token is missing")); - - if (!authToken.equals(apiToken)) - throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid API token")); - } -} From d19581a089a1904459bc445e11ee98f22b1a6c25 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 09:55:32 +0200 Subject: [PATCH 14/40] Reduce AuthHeaderCallCredentials visibility to package-private --- cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java b/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java index d7f98e7269a..d189b63e1c5 100644 --- a/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java +++ b/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java @@ -12,7 +12,7 @@ /** * Sets the {@value AUTH_HEADER_KEY} rpc call header to a given value. */ -public class AuthHeaderCallCredentials extends CallCredentials { +class AuthHeaderCallCredentials extends CallCredentials { public static final String AUTH_HEADER_KEY = "authorization"; From 0b338bbbea432e87f0818fd2e6e422af7d68ead9 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 09:59:22 +0200 Subject: [PATCH 15/40] Rename/repackage bisq.cli.{app.Bisq=>CliMain} --- build.gradle | 2 +- .../java/bisq/cli/{app => }/AuthHeaderCallCredentials.java | 2 +- .../main/java/bisq/cli/{app/BisqCliMain.java => CliMain.java} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename cli/src/main/java/bisq/cli/{app => }/AuthHeaderCallCredentials.java (98%) rename cli/src/main/java/bisq/cli/{app/BisqCliMain.java => CliMain.java} (99%) 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/app/AuthHeaderCallCredentials.java b/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java similarity index 98% rename from cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java rename to cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java index d189b63e1c5..67d3da3bd12 100644 --- a/cli/src/main/java/bisq/cli/app/AuthHeaderCallCredentials.java +++ b/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java @@ -1,4 +1,4 @@ -package bisq.cli.app; +package bisq.cli; import io.grpc.CallCredentials; import io.grpc.Metadata; diff --git a/cli/src/main/java/bisq/cli/app/BisqCliMain.java b/cli/src/main/java/bisq/cli/CliMain.java similarity index 99% rename from cli/src/main/java/bisq/cli/app/BisqCliMain.java rename to cli/src/main/java/bisq/cli/CliMain.java index 3e69b17b841..12925bba9e2 100644 --- a/cli/src/main/java/bisq/cli/app/BisqCliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.cli.app; +package bisq.cli; import bisq.proto.grpc.GetBalanceGrpc; import bisq.proto.grpc.GetBalanceRequest; @@ -46,7 +46,7 @@ * A command-line client for the Bisq gRPC API. */ @Slf4j -public class BisqCliMain { +public class CliMain { private static final int EXIT_SUCCESS = 0; private static final int EXIT_FAILURE = 1; From 423b2ad4aec45b6ca78c931d0a62ea1885a80a62 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 10:10:27 +0200 Subject: [PATCH 16/40] Handle OptionException gracefully When e.g. a given option is not recognized, print a clean error message and exit 1 as opposed to printing a stack trace. --- cli/src/main/java/bisq/cli/CliMain.java | 112 +++++++++++++----------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 12925bba9e2..6cac0fff507 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -25,6 +25,7 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import joptsimple.OptionException; import joptsimple.OptionParser; import java.text.DecimalFormat; @@ -69,69 +70,74 @@ public static void main(String[] args) throws IOException { var passwordOpt = parser.accepts("password", "Bisq node rpc server password") .withRequiredArg(); - var options = parser.parse(args); - - if (options.has(helpOpt)) { - out.println("Bisq RPC Client"); - out.println(); - out.println("Usage: bisq-cli [options] "); - out.println(); - parser.printHelpOn(out); - out.println(); - out.println("Command Descripiton"); - out.println("------- -----------"); - out.println("getversion Get Bisq node version"); - out.println("getbalance Get Bisq node wallet balance"); - out.println(); - exit(EXIT_SUCCESS); - } + try { + var options = parser.parse(args); + + if (options.has(helpOpt)) { + out.println("Bisq RPC Client"); + out.println(); + out.println("Usage: bisq-cli [options] "); + out.println(); + parser.printHelpOn(out); + out.println(); + out.println("Command Descripiton"); + out.println("------- -----------"); + out.println("getversion Get Bisq node version"); + out.println("getbalance Get Bisq node wallet balance"); + out.println(); + exit(EXIT_SUCCESS); + } - var host = options.valueOf(hostOpt); - var port = options.valueOf(portOpt); + var host = options.valueOf(hostOpt); + var port = options.valueOf(portOpt); - var password = options.valueOf(passwordOpt); - if (password == null) { - err.println("error: rpc password must not be null"); - exit(EXIT_FAILURE); - } + var password = options.valueOf(passwordOpt); + if (password == null) { + err.println("error: rpc password must not be null"); + exit(EXIT_FAILURE); + } - @SuppressWarnings("unchecked") - var nonOptionArgs = (List) options.nonOptionArguments(); - if (nonOptionArgs.isEmpty()) { - err.println("error: no rpc command specified"); - exit(EXIT_FAILURE); - } + @SuppressWarnings("unchecked") + var nonOptionArgs = (List) options.nonOptionArguments(); + if (nonOptionArgs.isEmpty()) { + err.println("error: no rpc command specified"); + exit(EXIT_FAILURE); + } - var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - var credentials = new AuthHeaderCallCredentials(password); + var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); + var credentials = new AuthHeaderCallCredentials(password); - var command = nonOptionArgs.get(0); + var command = nonOptionArgs.get(0); - if ("getversion".equals(command)) { - var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var request = GetVersionRequest.newBuilder().build(); - var version = stub.getVersion(request).getVersion(); - out.println(version); - shutdown(channel); - exit(EXIT_SUCCESS); - } + if ("getversion".equals(command)) { + var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetVersionRequest.newBuilder().build(); + var version = stub.getVersion(request).getVersion(); + out.println(version); + shutdown(channel); + exit(EXIT_SUCCESS); + } - if ("getbalance".equals(command)) { - var stub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var request = GetBalanceRequest.newBuilder().build(); - var balance = stub.getBalance(request).getBalance(); - if (balance == -1) { - err.println("Server initializing..."); + if ("getbalance".equals(command)) { + var stub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetBalanceRequest.newBuilder().build(); + var balance = stub.getBalance(request).getBalance(); + if (balance == -1) { + err.println("Server initializing..."); + shutdown(channel); + exit(EXIT_FAILURE); + } + out.println(formatBalance(balance)); shutdown(channel); - exit(EXIT_FAILURE); + exit(EXIT_SUCCESS); } - out.println(formatBalance(balance)); - shutdown(channel); - exit(EXIT_SUCCESS); - } - err.printf("error: unknown rpc command '%s'\n", command); - exit(EXIT_FAILURE); + err.printf("error: unknown rpc command '%s'\n", command); + exit(EXIT_FAILURE); + } catch (OptionException ex) { + err.println("error: " + ex.getMessage()); + exit(EXIT_FAILURE); + } } @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") From b67ad6cd74b74f851d8568df47a7c98545c63894 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 10:25:54 +0200 Subject: [PATCH 17/40] Print help text when no command is specified --- cli/src/main/java/bisq/cli/CliMain.java | 56 +++++++++++++++---------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 6cac0fff507..c9c4d842aa3 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -31,6 +31,7 @@ import java.text.DecimalFormat; import java.io.IOException; +import java.io.PrintStream; import java.math.BigDecimal; @@ -52,7 +53,7 @@ public class CliMain { private static final int EXIT_SUCCESS = 0; private static final int EXIT_FAILURE = 1; - public static void main(String[] args) throws IOException { + public static void main(String[] args) { var parser = new OptionParser(); var helpOpt = parser.accepts("help", "Print this help text") @@ -74,33 +75,24 @@ public static void main(String[] args) throws IOException { var options = parser.parse(args); if (options.has(helpOpt)) { - out.println("Bisq RPC Client"); - out.println(); - out.println("Usage: bisq-cli [options] "); - out.println(); - parser.printHelpOn(out); - out.println(); - out.println("Command Descripiton"); - out.println("------- -----------"); - out.println("getversion Get Bisq node version"); - out.println("getbalance Get Bisq node wallet balance"); - out.println(); + printHelp(parser, out); exit(EXIT_SUCCESS); } + @SuppressWarnings("unchecked") + var nonOptionArgs = (List) options.nonOptionArguments(); + if (nonOptionArgs.isEmpty()) { + printHelp(parser, err); + err.println("Error: no command specified"); + exit(EXIT_FAILURE); + } + var host = options.valueOf(hostOpt); var port = options.valueOf(portOpt); var password = options.valueOf(passwordOpt); if (password == null) { - err.println("error: rpc password must not be null"); - exit(EXIT_FAILURE); - } - - @SuppressWarnings("unchecked") - var nonOptionArgs = (List) options.nonOptionArguments(); - if (nonOptionArgs.isEmpty()) { - err.println("error: no rpc command specified"); + err.println("Error: no password specified"); exit(EXIT_FAILURE); } @@ -123,7 +115,7 @@ public static void main(String[] args) throws IOException { var request = GetBalanceRequest.newBuilder().build(); var balance = stub.getBalance(request).getBalance(); if (balance == -1) { - err.println("Server initializing..."); + err.println("Error: server is still initializing"); shutdown(channel); exit(EXIT_FAILURE); } @@ -132,14 +124,32 @@ public static void main(String[] args) throws IOException { exit(EXIT_SUCCESS); } - err.printf("error: unknown rpc command '%s'\n", command); + err.printf("Error: unknown command '%s'\n", command); exit(EXIT_FAILURE); } catch (OptionException ex) { - err.println("error: " + ex.getMessage()); + err.println("Error: " + ex.getMessage()); 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("Command Descripiton"); + stream.println("------- -----------"); + stream.println("getversion Get Bisq node version"); + stream.println("getbalance Get Bisq node wallet balance"); + stream.println(); + } catch (IOException ex) { + ex.printStackTrace(stream); + } + } + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") private static String formatBalance(long satoshis) { var btcFormat = new DecimalFormat("###,##0.00000000"); From c6e5670fbd9d65c1112da0776f96637aee447d94 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 10:42:53 +0200 Subject: [PATCH 18/40] Print nested exception error message on connect failure --- cli/src/main/java/bisq/cli/CliMain.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index c9c4d842aa3..b81293e8b01 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -24,6 +24,7 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.StatusRuntimeException; import joptsimple.OptionException; import joptsimple.OptionParser; @@ -125,9 +126,12 @@ public static void main(String[] args) { } err.printf("Error: unknown command '%s'\n", command); - exit(EXIT_FAILURE); } catch (OptionException ex) { err.println("Error: " + ex.getMessage()); + } catch (StatusRuntimeException ex) { + Throwable t = ex.getCause() == null ? ex : ex.getCause(); + err.println("Error: " + t.getMessage()); + } finally { exit(EXIT_FAILURE); } } From 7bf854c86e4fb5b1474be7e740f01e95fec95bbf Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 11:22:01 +0200 Subject: [PATCH 19/40] Improve error handling Fail fast if the specified command is unknown --- .../bisq/cli/AuthHeaderCallCredentials.java | 3 + cli/src/main/java/bisq/cli/CliMain.java | 64 +++++++++++-------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java b/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java index 67d3da3bd12..f90919ad03a 100644 --- a/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java +++ b/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java @@ -8,6 +8,7 @@ import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static io.grpc.Status.UNAUTHENTICATED; +import static java.lang.String.format; /** * Sets the {@value AUTH_HEADER_KEY} rpc call header to a given value. @@ -19,6 +20,8 @@ class AuthHeaderCallCredentials extends CallCredentials { private final String authHeaderValue; public AuthHeaderCallCredentials(String authHeaderValue) { + if (authHeaderValue == null) + throw new IllegalArgumentException(format("'%s' header value must not be null", AUTH_HEADER_KEY)); this.authHeaderValue = authHeaderValue; } diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index b81293e8b01..d05bbdba0b6 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -54,6 +54,11 @@ public class CliMain { private static final int EXIT_SUCCESS = 0; private static final int EXIT_FAILURE = 1; + private enum Command { + getversion, + getbalance + } + public static void main(String[] args) { var parser = new OptionParser(); @@ -88,50 +93,59 @@ public static void main(String[] args) { exit(EXIT_FAILURE); } + var commandName = nonOptionArgs.get(0); + Command command = null; + try { + command = Command.valueOf(commandName); + } catch (IllegalArgumentException ex) { + err.printf("Error: '%s' is not a supported command\n", commandName); + exit(EXIT_FAILURE); + } + var host = options.valueOf(hostOpt); var port = options.valueOf(portOpt); - var password = options.valueOf(passwordOpt); if (password == null) { - err.println("Error: no password specified"); + err.println("Error: rpc server password not specified"); exit(EXIT_FAILURE); } var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); var credentials = new AuthHeaderCallCredentials(password); - var command = nonOptionArgs.get(0); - - if ("getversion".equals(command)) { - var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var request = GetVersionRequest.newBuilder().build(); - var version = stub.getVersion(request).getVersion(); - out.println(version); - shutdown(channel); - exit(EXIT_SUCCESS); - } - - if ("getbalance".equals(command)) { - 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"); + switch (command) { + case getversion: { + var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetVersionRequest.newBuilder().build(); + var version = stub.getVersion(request).getVersion(); + out.println(version); + shutdown(channel); + 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"); + shutdown(channel); + exit(EXIT_FAILURE); + } + out.println(formatBalance(balance)); shutdown(channel); + exit(EXIT_SUCCESS); + } + default: { + err.printf("Error: unhandled command '%s'\n", command); exit(EXIT_FAILURE); } - out.println(formatBalance(balance)); - shutdown(channel); - exit(EXIT_SUCCESS); } - - err.printf("Error: unknown command '%s'\n", command); } catch (OptionException ex) { err.println("Error: " + ex.getMessage()); + exit(EXIT_FAILURE); } catch (StatusRuntimeException ex) { Throwable t = ex.getCause() == null ? ex : ex.getCause(); err.println("Error: " + t.getMessage()); - } finally { exit(EXIT_FAILURE); } } From c1931d2fb28427156ef00aa1199659357cf7085f Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 11:24:33 +0200 Subject: [PATCH 20/40] Use 'method' vs 'command' naming --- cli/src/main/java/bisq/cli/CliMain.java | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index d05bbdba0b6..8332c0c5cb1 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -54,7 +54,7 @@ public class CliMain { private static final int EXIT_SUCCESS = 0; private static final int EXIT_FAILURE = 1; - private enum Command { + private enum Method { getversion, getbalance } @@ -89,16 +89,16 @@ public static void main(String[] args) { var nonOptionArgs = (List) options.nonOptionArguments(); if (nonOptionArgs.isEmpty()) { printHelp(parser, err); - err.println("Error: no command specified"); + err.println("Error: no method specified"); exit(EXIT_FAILURE); } - var commandName = nonOptionArgs.get(0); - Command command = null; + var methodName = nonOptionArgs.get(0); + Method method = null; try { - command = Command.valueOf(commandName); + method = Method.valueOf(methodName); } catch (IllegalArgumentException ex) { - err.printf("Error: '%s' is not a supported command\n", commandName); + err.printf("Error: '%s' is not a supported method\n", methodName); exit(EXIT_FAILURE); } @@ -113,7 +113,7 @@ public static void main(String[] args) { var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); var credentials = new AuthHeaderCallCredentials(password); - switch (command) { + switch (method) { case getversion: { var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); var request = GetVersionRequest.newBuilder().build(); @@ -136,7 +136,7 @@ public static void main(String[] args) { exit(EXIT_SUCCESS); } default: { - err.printf("Error: unhandled command '%s'\n", command); + err.printf("Error: unhandled method '%s'\n", method); exit(EXIT_FAILURE); } } @@ -154,11 +154,11 @@ private static void printHelp(OptionParser parser, PrintStream stream) { try { stream.println("Bisq RPC Client"); stream.println(); - stream.println("Usage: bisq-cli [options] "); + stream.println("Usage: bisq-cli [options] "); stream.println(); parser.printHelpOn(stream); stream.println(); - stream.println("Command Descripiton"); + stream.println("Method Descripiton"); stream.println("------- -----------"); stream.println("getversion Get Bisq node version"); stream.println("getbalance Get Bisq node wallet balance"); From 42e9bb143301214b41b18ce4743400b21cb7ac06 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 11:27:28 +0200 Subject: [PATCH 21/40] Refine help output --- cli/src/main/java/bisq/cli/CliMain.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 8332c0c5cb1..ca8fef82e61 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -65,16 +65,16 @@ public static void main(String[] args) { var helpOpt = parser.accepts("help", "Print this help text") .forHelp(); - var hostOpt = parser.accepts("host", "Bisq node hostname or IP") + var hostOpt = parser.accepts("host", "rpc server hostname or IP") .withRequiredArg() .defaultsTo("localhost"); - var portOpt = parser.accepts("port", "Bisq node rpc port") + var portOpt = parser.accepts("port", "rpc server port") .withRequiredArg() .ofType(Integer.class) .defaultsTo(9998); - var passwordOpt = parser.accepts("password", "Bisq node rpc server password") + var passwordOpt = parser.accepts("password", "rpc server password") .withRequiredArg(); try { @@ -158,10 +158,10 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.println(); parser.printHelpOn(stream); stream.println(); - stream.println("Method Descripiton"); - stream.println("------- -----------"); - stream.println("getversion Get Bisq node version"); - stream.println("getbalance Get Bisq node wallet balance"); + stream.println("Method Descripiton"); + stream.println("------- -----------"); + stream.println("getversion Get server version"); + stream.println("getbalance Get server wallet balance"); stream.println(); } catch (IOException ex) { ex.printStackTrace(stream); From 653856f79bfd905fefe2975c6cec78804fa9d15b Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 13:28:10 +0200 Subject: [PATCH 22/40] Refactor 'auth*' naming to 'password' To increase simplicity and make the implementation more intention-revealing. --- cli/src/main/java/bisq/cli/CliMain.java | 2 +- ...ials.java => PasswordCallCredentials.java} | 20 ++++++++--------- .../java/bisq/core/grpc/BisqGrpcServer.java | 2 +- ...ptor.java => PasswordAuthInterceptor.java} | 22 +++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) rename cli/src/main/java/bisq/cli/{AuthHeaderCallCredentials.java => PasswordCallCredentials.java} (59%) rename core/src/main/java/bisq/core/grpc/{AuthorizationInterceptor.java => PasswordAuthInterceptor.java} (56%) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index ca8fef82e61..aa13a3d849e 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -111,7 +111,7 @@ public static void main(String[] args) { } var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - var credentials = new AuthHeaderCallCredentials(password); + var credentials = new PasswordCallCredentials(password); switch (method) { case getversion: { diff --git a/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java similarity index 59% rename from cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java rename to cli/src/main/java/bisq/cli/PasswordCallCredentials.java index f90919ad03a..3a03a8a0680 100644 --- a/cli/src/main/java/bisq/cli/AuthHeaderCallCredentials.java +++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java @@ -11,18 +11,18 @@ import static java.lang.String.format; /** - * Sets the {@value AUTH_HEADER_KEY} rpc call header to a given value. + * Sets the {@value PASSWORD_KEY} rpc call header to a given value. */ -class AuthHeaderCallCredentials extends CallCredentials { +class PasswordCallCredentials extends CallCredentials { - public static final String AUTH_HEADER_KEY = "authorization"; + public static final String PASSWORD_KEY = "password"; - private final String authHeaderValue; + private final String passwordValue; - public AuthHeaderCallCredentials(String authHeaderValue) { - if (authHeaderValue == null) - throw new IllegalArgumentException(format("'%s' header value must not be null", AUTH_HEADER_KEY)); - this.authHeaderValue = authHeaderValue; + public PasswordCallCredentials(String passwordValue) { + if (passwordValue == null) + throw new IllegalArgumentException(format("'%s' header value must not be null", PASSWORD_KEY)); + this.passwordValue = passwordValue; } @Override @@ -30,8 +30,8 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, appExecutor.execute(() -> { try { var headers = new Metadata(); - var authorizationKey = Key.of(AUTH_HEADER_KEY, ASCII_STRING_MARSHALLER); - headers.put(authorizationKey, authHeaderValue); + var passwordKey = Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); + headers.put(passwordKey, passwordValue); metadataApplier.apply(headers); } catch (Throwable ex) { metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); diff --git a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java index 5c434831a8a..9ce22b72db8 100644 --- a/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java +++ b/core/src/main/java/bisq/core/grpc/BisqGrpcServer.java @@ -215,7 +215,7 @@ private void start() throws IOException { .addService(new GetPaymentAccountsImpl()) .addService(new PlaceOfferImpl()) .addService(new StopServerImpl()) - .intercept(new AuthorizationInterceptor(config.apiPassword)) + .intercept(new PasswordAuthInterceptor(config.apiPassword)) .build() .start(); diff --git a/core/src/main/java/bisq/core/grpc/AuthorizationInterceptor.java b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java similarity index 56% rename from core/src/main/java/bisq/core/grpc/AuthorizationInterceptor.java rename to core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java index de8baf92ae2..09290442a90 100644 --- a/core/src/main/java/bisq/core/grpc/AuthorizationInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java @@ -15,33 +15,33 @@ /** * Authorizes rpc server calls by comparing the value of the caller's - * {@value AUTH_HEADER_KEY} header to an expected value set at server startup time. + * {@value PASSWORD_KEY} header to an expected value set at server startup time. * * @see bisq.common.config.Config#apiPassword */ @Slf4j -public class AuthorizationInterceptor implements ServerInterceptor { +public class PasswordAuthInterceptor implements ServerInterceptor { - public static final String AUTH_HEADER_KEY = "authorization"; + public static final String PASSWORD_KEY = "password"; - private final String expectedAuthHeaderValue; + private final String expectedPasswordValue; - public AuthorizationInterceptor(String expectedAuthHeaderValue) { - this.expectedAuthHeaderValue = expectedAuthHeaderValue; + public PasswordAuthInterceptor(String expectedPasswordValue) { + this.expectedPasswordValue = expectedPasswordValue; } @Override public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata headers, ServerCallHandler serverCallHandler) { - var actualAuthHeaderValue = headers.get(Key.of(AUTH_HEADER_KEY, ASCII_STRING_MARSHALLER)); + var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER)); - if (actualAuthHeaderValue == null) + if (actualPasswordValue == null) throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( - format("missing '%s' rpc header value", AUTH_HEADER_KEY))); + format("missing '%s' rpc header value", PASSWORD_KEY))); - if (!actualAuthHeaderValue.equals(expectedAuthHeaderValue)) + if (!actualPasswordValue.equals(expectedPasswordValue)) throw new StatusRuntimeException(UNAUTHENTICATED.withDescription( - format("incorrect '%s' rpc header value", AUTH_HEADER_KEY))); + format("incorrect '%s' rpc header value", PASSWORD_KEY))); return serverCallHandler.startCall(serverCall, headers); } From 20f563ae17472e3ca3549abb63bb6f633813392f Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 13:32:40 +0200 Subject: [PATCH 23/40] Reduce PasswordAuthInterceptor visibility to package-private And remove unused @Slf4j lombok annotation --- .../main/java/bisq/core/grpc/PasswordAuthInterceptor.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java index 09290442a90..2ab29bcdc95 100644 --- a/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java +++ b/core/src/main/java/bisq/core/grpc/PasswordAuthInterceptor.java @@ -6,8 +6,6 @@ import io.grpc.ServerInterceptor; import io.grpc.StatusRuntimeException; -import lombok.extern.slf4j.Slf4j; - import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static io.grpc.Metadata.Key; import static io.grpc.Status.UNAUTHENTICATED; @@ -19,8 +17,7 @@ * * @see bisq.common.config.Config#apiPassword */ -@Slf4j -public class PasswordAuthInterceptor implements ServerInterceptor { +class PasswordAuthInterceptor implements ServerInterceptor { public static final String PASSWORD_KEY = "password"; From d923d07781934e958204301f9e87935a32b522f1 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 25 Apr 2020 13:34:32 +0200 Subject: [PATCH 24/40] Fix typo in help output --- cli/src/main/java/bisq/cli/CliMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index aa13a3d849e..89b4a86fc57 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -158,7 +158,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.println(); parser.printHelpOn(stream); stream.println(); - stream.println("Method Descripiton"); + stream.println("Method Description"); stream.println("------- -----------"); stream.println("getversion Get server version"); stream.println("getbalance Get server wallet balance"); From 16c2efc8e143214f755f2a9fa0bdadc5a606f8b3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 25 Apr 2020 17:52:54 -0300 Subject: [PATCH 25/40] Allow double-quoted multiword apiPassword Strip double quotes from :cli --password arg before it is passed to server. Otherwise, a correct "password" will fail authentication. --- cli/src/main/java/bisq/cli/PasswordCallCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java index 3a03a8a0680..dd7d8b92214 100644 --- a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java +++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java @@ -22,7 +22,7 @@ class PasswordCallCredentials extends CallCredentials { public PasswordCallCredentials(String passwordValue) { if (passwordValue == null) throw new IllegalArgumentException(format("'%s' header value must not be null", PASSWORD_KEY)); - this.passwordValue = passwordValue; + this.passwordValue = passwordValue.replace("\"", ""); } @Override From 56f2923ade7ad0cd21a4c3c7cb56ab9d18c64e12 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 25 Apr 2020 18:16:23 -0300 Subject: [PATCH 26/40] Remove redundant text from console err msg The client was displaying Error: UNAUTHENTICATED: incorrect 'password' rpc header value from the StatusRuntimeException message. Strip "UNAUTHENTICATED: " from the exception message string before printing it. --- cli/src/main/java/bisq/cli/CliMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 89b4a86fc57..7cb7b34ee32 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -145,7 +145,7 @@ public static void main(String[] args) { exit(EXIT_FAILURE); } catch (StatusRuntimeException ex) { Throwable t = ex.getCause() == null ? ex : ex.getCause(); - err.println("Error: " + t.getMessage()); + err.println("Error: " + t.getMessage().replace("UNAUTHENTICATED: ", "")); exit(EXIT_FAILURE); } } From dee5e4cf7ec5c5150c697efe9af219329803f9e6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 26 Apr 2020 10:29:01 -0300 Subject: [PATCH 27/40] Revert 16c2efc No need to strip quotes from password. The problem was a bug in my expect/tcl script's quote escape syntax. --- cli/src/main/java/bisq/cli/PasswordCallCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java index dd7d8b92214..3a03a8a0680 100644 --- a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java +++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java @@ -22,7 +22,7 @@ class PasswordCallCredentials extends CallCredentials { public PasswordCallCredentials(String passwordValue) { if (passwordValue == null) throw new IllegalArgumentException(format("'%s' header value must not be null", PASSWORD_KEY)); - this.passwordValue = passwordValue.replace("\"", ""); + this.passwordValue = passwordValue; } @Override From e10e29a211a3eb7d2fe146aa59d19142a4384de3 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 19:30:35 +0200 Subject: [PATCH 28/40] Handle OptionException immediately --- cli/src/main/java/bisq/cli/CliMain.java | 64 +++++++++++++------------ 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 7cb7b34ee32..6f8b394b893 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -28,6 +28,7 @@ import joptsimple.OptionException; import joptsimple.OptionParser; +import joptsimple.OptionSet; import java.text.DecimalFormat; @@ -77,39 +78,45 @@ public static void main(String[] args) { var passwordOpt = parser.accepts("password", "rpc server password") .withRequiredArg(); + OptionSet options = null; try { - var options = parser.parse(args); + 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); - } + 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); - } + @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 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: rpc server password not specified"); - exit(EXIT_FAILURE); - } + var host = options.valueOf(hostOpt); + var port = options.valueOf(portOpt); + var password = options.valueOf(passwordOpt); + if (password == null) { + err.println("Error: rpc server password not specified"); + exit(EXIT_FAILURE); + } + try { var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); var credentials = new PasswordCallCredentials(password); @@ -140,9 +147,6 @@ public static void main(String[] args) { exit(EXIT_FAILURE); } } - } catch (OptionException ex) { - err.println("Error: " + ex.getMessage()); - exit(EXIT_FAILURE); } catch (StatusRuntimeException ex) { Throwable t = ex.getCause() == null ? ex : ex.getCause(); err.println("Error: " + t.getMessage().replace("UNAUTHENTICATED: ", "")); From 0a2aac00d3da9c2c7e3db160d7573329635252bf Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 19:35:48 +0200 Subject: [PATCH 29/40] Shutdown channel using a JVM shutdown hook Versus needing to manage calling custom shutdown method in all the right places. --- cli/src/main/java/bisq/cli/CliMain.java | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 6f8b394b893..837dff0ab45 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -22,7 +22,6 @@ import bisq.proto.grpc.GetVersionGrpc; import bisq.proto.grpc.GetVersionRequest; -import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; @@ -118,6 +117,15 @@ public static void main(String[] args) { try { 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); + } + })); + var credentials = new PasswordCallCredentials(password); switch (method) { @@ -126,7 +134,6 @@ public static void main(String[] args) { var request = GetVersionRequest.newBuilder().build(); var version = stub.getVersion(request).getVersion(); out.println(version); - shutdown(channel); exit(EXIT_SUCCESS); } case getbalance: { @@ -135,11 +142,9 @@ public static void main(String[] args) { var balance = stub.getBalance(request).getBalance(); if (balance == -1) { err.println("Error: server is still initializing"); - shutdown(channel); exit(EXIT_FAILURE); } out.println(formatBalance(balance)); - shutdown(channel); exit(EXIT_SUCCESS); } default: { @@ -178,12 +183,4 @@ private static String formatBalance(long satoshis) { var satoshiDivisor = new BigDecimal(100000000); return btcFormat.format(BigDecimal.valueOf(satoshis).divide(satoshiDivisor)); } - - private static void shutdown(ManagedChannel channel) { - try { - channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - } } From b7fda8de5e7a18e3c9829f599249cb76657421a5 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 19:38:06 +0200 Subject: [PATCH 30/40] Declare channel outside try/catch No StatusRuntimeException can be thrown from this code, so it is not necessary to include in the try block. --- cli/src/main/java/bisq/cli/CliMain.java | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 837dff0ab45..342423948b0 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -115,17 +115,17 @@ public static void main(String[] args) { exit(EXIT_FAILURE); } - try { - 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); - } - })); + 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 { var credentials = new PasswordCallCredentials(password); switch (method) { From 510e84d30a244a62067bef490ec7e9ea903f042c Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 20:30:58 +0200 Subject: [PATCH 31/40] Add expect-based cli test suite Co-authored-by: ghubstan <36207203+ghubstan@users.noreply.github.com> --- cli-test.sh | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100755 cli-test.sh diff --git a/cli-test.sh b/cli-test.sh new file mode 100755 index 00000000000..0eddf7373d4 --- /dev/null +++ b/cli-test.sh @@ -0,0 +1,165 @@ +#! /bin/bash + +# Some 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 + +# Requirements for these test cases: +# +# :daemon -> ./bisq-daemon --apiPassword=xyz +# :cli -> unencrypted wallet with balance = 0.00000000 BTC +# test script must be copied to bisq project root dir +# usage: ./cli-test.sh + + +OUTPUT=$(expect -c ' + # exp_internal 1 + puts "TEST unsupported cmd error" + set expected "Error: '\''peanutbutter'\'' is not a supported method" + spawn ./bisq-cli --password=xyz peanutbutter + 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 bad option error" + set expected "Error: pwd is not a recognized option" + spawn ./bisq-cli --pwd=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 ' + # exp_internal 1 + puts "TEST getversion (no pwd error)" + set expected "Error: rpc server password not specified" + 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 (bad pwd error)" + set expected "Error: incorrect '\''password'\'' rpc header value" + spawn ./bisq-cli --password=badpassword 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 (pwd in quotes) COMMIT" + 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 (no pwd error)" + # exp_internal 1 + set expected "Error: rpc server password not specified" + spawn ./bisq-cli 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 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 "========================================================================" + +echo "TEST help (todo)" +./bisq-cli --password=xyz --help + + + From ceaf20a161769a3ab315f9ef83e4e1a5bd923560 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 20:35:09 +0200 Subject: [PATCH 32/40] Clean up whitespace in cli-test.sh --- cli-test.sh | 195 +++++++++++++++++++++++++--------------------------- 1 file changed, 92 insertions(+), 103 deletions(-) diff --git a/cli-test.sh b/cli-test.sh index 0eddf7373d4..569aabe0d87 100755 --- a/cli-test.sh +++ b/cli-test.sh @@ -1,165 +1,154 @@ -#! /bin/bash - +#!/bin/bash +# # Some 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 - +# https://www.oreilly.com/library/view/exploring-expect/9781565920903/ch04.html +# # Requirements for these test cases: # -# :daemon -> ./bisq-daemon --apiPassword=xyz -# :cli -> unencrypted wallet with balance = 0.00000000 BTC -# test script must be copied to bisq project root dir -# usage: ./cli-test.sh +# :daemon -> ./bisq-daemon --apiPassword=xyz +# :cli -> unencrypted wallet with balance = 0.00000000 BTC +# test script must be copied to bisq project root dir +# usage: ./cli-test.sh - -OUTPUT=$(expect -c ' - # exp_internal 1 - puts "TEST unsupported cmd error" - set expected "Error: '\''peanutbutter'\'' is not a supported method" - spawn ./bisq-cli --password=xyz peanutbutter +OUTPUT=$(expect -c ' + # exp_internal 1 + puts "TEST unsupported cmd error" + set expected "Error: '\''peanutbutter'\'' is not a supported method" + spawn ./bisq-cli --password=xyz peanutbutter expect { - $expected { puts "PASS" } - default { - set results $expect_out(buffer) - puts "FAIL expected = $expected" - puts " actual = $results" - } - } + $expected { puts "PASS" } + default { + set results $expect_out(buffer) + puts "FAIL expected = $expected" + puts " actual = $results" + } + } ') echo "$OUTPUT" echo "========================================================================" OUTPUT=$(expect -c ' - puts "TEST bad option error" - set expected "Error: pwd is not a recognized option" + puts "TEST bad option error" + set expected "Error: pwd is not a recognized option" spawn ./bisq-cli --pwd=xyz getversion expect { - $expected { puts "PASS" } - default { - set results $expect_out(buffer) - puts "FAIL expected = $expected" - puts " actual = $results" - } + $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 (no pwd error)" - set expected "Error: rpc server password not specified" - spawn ./bisq-cli getversion +OUTPUT=$(expect -c ' + # exp_internal 1 + puts "TEST getversion (no pwd error)" + set expected "Error: rpc server password not specified" + spawn ./bisq-cli getversion expect { - $expected { puts "PASS" } - default { - set results $expect_out(buffer) - puts "FAIL expected = $expected" - puts " actual = $results" - } - } + $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 (bad pwd error)" - set expected "Error: incorrect '\''password'\'' rpc header value" + puts "TEST getversion (bad pwd error)" + set expected "Error: incorrect '\''password'\'' rpc header value" spawn ./bisq-cli --password=badpassword getversion expect { - $expected { puts "PASS\n" } - default { - set results $expect_out(buffer) - puts "FAIL expected = $expected" - puts " actual = $results" - } + $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 (pwd in quotes) COMMIT" - 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 + puts "TEST getversion (pwd in quotes) COMMIT" + 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" - } + $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" + 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" - } + $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 (no pwd error)" - # exp_internal 1 - set expected "Error: rpc server password not specified" - spawn ./bisq-cli getbalance +OUTPUT=$(expect -c ' + puts "TEST getbalance (no pwd error)" + # exp_internal 1 + set expected "Error: rpc server password not specified" + spawn ./bisq-cli getbalance expect { - $expected { puts "PASS" } - default { - set results $expect_out(buffer) - puts "FAIL expected = $expected" - puts " actual = $results" - } - } + $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 +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" - } - } + $expected { puts "PASS" } + default { + set results $expect_out(buffer) + puts "FAIL expected = $expected" + puts " actual = $results" + } + } ') echo "$OUTPUT" - - echo "========================================================================" -echo "TEST help (todo)" +echo "TEST help (todo)" ./bisq-cli --password=xyz --help - - - From a3f9faf7ab1cbc79a2b541171d7bc314846b4515 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 20:46:52 +0200 Subject: [PATCH 33/40] Move cli-test.sh => cli/test.sh - Ensure script runs from root directory regardless of where it is invoked from - Touch up documentation and whitespace --- cli-test.sh => cli/test.sh | 41 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) rename cli-test.sh => cli/test.sh (78%) diff --git a/cli-test.sh b/cli/test.sh similarity index 78% rename from cli-test.sh rename to cli/test.sh index 569aabe0d87..3d9d615b934 100755 --- a/cli-test.sh +++ b/cli/test.sh @@ -1,23 +1,26 @@ #!/bin/bash # -# Some 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 +# References & examples for expect: # -# Requirements for these test cases: +# - 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 # -# :daemon -> ./bisq-daemon --apiPassword=xyz -# :cli -> unencrypted wallet with balance = 0.00000000 BTC -# test script must be copied to bisq project root dir -# usage: ./cli-test.sh +# Prior to running this script, run: +# +# ./bisq-daemon --apiPassword=xyz --appDataDir=$(mktemp -d) +# +# The fresh data directory ensures a new, unencrypted wallet with 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: '\''peanutbutter'\'' is not a supported method" - spawn ./bisq-cli --password=xyz peanutbutter + set expected "Error: '\''bogus'\'' is not a supported method" + spawn ./bisq-cli --password=xyz bogus expect { $expected { puts "PASS" } default { @@ -33,7 +36,7 @@ echo "========================================================================" OUTPUT=$(expect -c ' puts "TEST bad option error" set expected "Error: pwd is not a recognized option" - spawn ./bisq-cli --pwd=xyz getversion + spawn ./bisq-cli --pwd=xyz getversion expect { $expected { puts "PASS" } default { @@ -50,7 +53,7 @@ OUTPUT=$(expect -c ' # exp_internal 1 puts "TEST getversion (no pwd error)" set expected "Error: rpc server password not specified" - spawn ./bisq-cli getversion + spawn ./bisq-cli getversion expect { $expected { puts "PASS" } default { @@ -67,7 +70,7 @@ OUTPUT=$(expect -c ' # exp_internal 1 puts "TEST getversion (bad pwd error)" set expected "Error: incorrect '\''password'\'' rpc header value" - spawn ./bisq-cli --password=badpassword getversion + spawn ./bisq-cli --password=badpassword getversion expect { $expected { puts "PASS\n" } default { @@ -84,7 +87,7 @@ OUTPUT=$(expect -c ' # exp_internal 1 puts "TEST getversion (pwd in quotes) COMMIT" set expected "1.3.2" - # Note: have to define quoted argument in a variable as "''value''" + # 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 { @@ -102,7 +105,7 @@ echo "========================================================================" OUTPUT=$(expect -c ' puts "TEST getversion" set expected "1.3.2" - spawn ./bisq-cli --password=xyz getversion + spawn ./bisq-cli --password=xyz getversion expect { $expected { puts "PASS" } default { @@ -136,7 +139,7 @@ OUTPUT=$(expect -c ' puts "TEST getbalance" # exp_internal 1 set expected "0.00000000" - spawn ./bisq-cli --password=xyz getbalance + spawn ./bisq-cli --password=xyz getbalance expect { $expected { puts "PASS" } default { @@ -151,4 +154,4 @@ echo "$OUTPUT" echo "========================================================================" echo "TEST help (todo)" -./bisq-cli --password=xyz --help +./bisq-cli --password=xyz --help From 822e6813f896d38744e7cd4c01589d5252b9bd86 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 21:04:06 +0200 Subject: [PATCH 34/40] Touch up test descriptions and parameter naming --- cli/test.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/test.sh b/cli/test.sh index 3d9d615b934..d1bac098fa8 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -34,9 +34,9 @@ echo "$OUTPUT" echo "========================================================================" OUTPUT=$(expect -c ' - puts "TEST bad option error" - set expected "Error: pwd is not a recognized option" - spawn ./bisq-cli --pwd=xyz getversion + puts "TEST unrecognized option error" + set expected "Error: bogus is not a recognized option" + spawn ./bisq-cli --bogus getversion expect { $expected { puts "PASS" } default { @@ -68,9 +68,9 @@ echo "========================================================================" OUTPUT=$(expect -c ' # exp_internal 1 - puts "TEST getversion (bad pwd error)" + puts "TEST getversion (incorrect password error)" set expected "Error: incorrect '\''password'\'' rpc header value" - spawn ./bisq-cli --password=badpassword getversion + spawn ./bisq-cli --password=bogus getversion expect { $expected { puts "PASS\n" } default { @@ -85,7 +85,7 @@ echo "========================================================================" OUTPUT=$(expect -c ' # exp_internal 1 - puts "TEST getversion (pwd in quotes) COMMIT" + puts "TEST getversion (password value in quotes) COMMIT" set expected "1.3.2" # Note: have to define quoted argument in a variable as "''value''" set pwd_in_quotes "''xyz''" From 5fa7939ec449da425d79272cec4a9d6a0635380a Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 21:05:22 +0200 Subject: [PATCH 35/40] Set --password as a required option in JOpt parser --- cli/src/main/java/bisq/cli/CliMain.java | 11 ++++----- .../bisq/cli/PasswordCallCredentials.java | 2 +- cli/test.sh | 23 +++---------------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 342423948b0..2554ac4a317 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -75,7 +75,8 @@ public static void main(String[] args) { .defaultsTo(9998); var passwordOpt = parser.accepts("password", "rpc server password") - .withRequiredArg(); + .withRequiredArg() + .required(); OptionSet options = null; try { @@ -110,10 +111,8 @@ public static void main(String[] args) { var host = options.valueOf(hostOpt); var port = options.valueOf(portOpt); var password = options.valueOf(passwordOpt); - if (password == null) { - err.println("Error: rpc server password not specified"); - exit(EXIT_FAILURE); - } + + var credentials = new PasswordCallCredentials(password); var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -126,8 +125,6 @@ public static void main(String[] args) { })); try { - var credentials = new PasswordCallCredentials(password); - switch (method) { case getversion: { var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); diff --git a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java index 3a03a8a0680..14b451d28f8 100644 --- a/cli/src/main/java/bisq/cli/PasswordCallCredentials.java +++ b/cli/src/main/java/bisq/cli/PasswordCallCredentials.java @@ -21,7 +21,7 @@ class PasswordCallCredentials extends CallCredentials { public PasswordCallCredentials(String passwordValue) { if (passwordValue == null) - throw new IllegalArgumentException(format("'%s' header value must not be null", PASSWORD_KEY)); + throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); this.passwordValue = passwordValue; } diff --git a/cli/test.sh b/cli/test.sh index d1bac098fa8..33d0439d89d 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -51,9 +51,9 @@ echo "========================================================================" OUTPUT=$(expect -c ' # exp_internal 1 - puts "TEST getversion (no pwd error)" - set expected "Error: rpc server password not specified" - spawn ./bisq-cli getversion + puts "TEST missing required password option error" + set expected "Error: Missing required option(s) \\\[password\\\]" + spawn ./bisq-cli anymethod expect { $expected { puts "PASS" } default { @@ -118,23 +118,6 @@ OUTPUT=$(expect -c ' echo "$OUTPUT" echo "========================================================================" -OUTPUT=$(expect -c ' - puts "TEST getbalance (no pwd error)" - # exp_internal 1 - set expected "Error: rpc server password not specified" - spawn ./bisq-cli 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 getbalance" # exp_internal 1 From f4b4f5ad893e38e0aa0cf640f2c32c33d11379f6 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 21:10:25 +0200 Subject: [PATCH 36/40] Update cli/test.sh instructions Don't instruct the user to create a fresh data directory every time, as this takes quite a bit longer to initialize the wallet than running against the same data directory repeatedly. Just be clear that the requirement is an unencrypted wallet with 0 BTC balance. --- cli/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/test.sh b/cli/test.sh index 33d0439d89d..0cc2435f472 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -9,9 +9,9 @@ # # Prior to running this script, run: # -# ./bisq-daemon --apiPassword=xyz --appDataDir=$(mktemp -d) +# ./bisq-daemon --apiPassword=xyz # -# The fresh data directory ensures a new, unencrypted wallet with 0 BTC balance +# 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)/.. From f5803492bdc0e2353254dd558e3ad3b9d28c6216 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 21:27:24 +0200 Subject: [PATCH 37/40] Add comment explaining exception message mangling --- cli/src/main/java/bisq/cli/CliMain.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 2554ac4a317..3d78c0e85f6 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -150,6 +150,9 @@ public static void main(String[] args) { } } } 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); From a6a8702084a3ee8bc36192d22bace89c929f54c0 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 21:29:29 +0200 Subject: [PATCH 38/40] Touch up help output Stop attempting to align Option and Method description columns with one another. It's a moving target as we add options and method names, and this help output format will probably change in the future anyway. --- cli/src/main/java/bisq/cli/CliMain.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 3d78c0e85f6..abd9d2ac870 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -167,10 +167,10 @@ private static void printHelp(OptionParser parser, PrintStream stream) { 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("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); From 312ef30b7095d4b6caf6245a9d857b07f28fafcc Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sun, 26 Apr 2020 21:44:03 +0200 Subject: [PATCH 39/40] Revert marking password as required in JOpt parser This is a partial reversion of the earlier commit. Marking the password option as required at the parser level made it impossible to run ./bisq-cli without options or arguments and get the help text. This is a useful thing to do, and not worth creating a bad user experience to get the free required option error handling and error messaging. --- cli/src/main/java/bisq/cli/CliMain.java | 7 +++++-- cli/test.sh | 24 ++++++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index abd9d2ac870..27f40b68b6c 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -75,8 +75,7 @@ public static void main(String[] args) { .defaultsTo(9998); var passwordOpt = parser.accepts("password", "rpc server password") - .withRequiredArg() - .required(); + .withRequiredArg(); OptionSet options = null; try { @@ -111,6 +110,10 @@ public static void main(String[] args) { 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); diff --git a/cli/test.sh b/cli/test.sh index 0cc2435f472..b3739a24240 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -52,8 +52,8 @@ echo "========================================================================" OUTPUT=$(expect -c ' # exp_internal 1 puts "TEST missing required password option error" - set expected "Error: Missing required option(s) \\\[password\\\]" - spawn ./bisq-cli anymethod + set expected "Error: missing required '\''password'\'' option" + spawn ./bisq-cli getversion expect { $expected { puts "PASS" } default { @@ -133,8 +133,24 @@ OUTPUT=$(expect -c ' } ') 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 (todo)" -./bisq-cli --password=xyz --help +echo "TEST --help option prints help text" +./bisq-cli --help From cfb7e32e70da54efa2be58f25f50d0589138e006 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 26 Apr 2020 16:54:10 -0300 Subject: [PATCH 40/40] Remove note to self --- cli/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/test.sh b/cli/test.sh index b3739a24240..046cbd910aa 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -85,7 +85,7 @@ echo "========================================================================" OUTPUT=$(expect -c ' # exp_internal 1 - puts "TEST getversion (password value in quotes) COMMIT" + 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''"