diff --git a/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java b/cli/src/main/java/bisq/cli/app/BisqCallCredentials.java index b3e92079c4..2f8af1e00c 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.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; @@ -17,10 +14,10 @@ import static io.grpc.Status.UNAUTHENTICATED; */ 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 class BisqCallCredentials extends CallCredentials { 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 class BisqCallCredentials extends CallCredentials { }); } - 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 dbe2d1af33..57c90418b2 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 @@ final class CliConfig { } 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 827dd4d404..e51cc1578c 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.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 { private Optional cmdToken; - private final Map creds = new HashMap<>(); + private String apiToken; private final CliConfig config; public CommandParser(CliConfig config) { @@ -25,37 +19,19 @@ final class CommandParser { } 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 b8455e7711..06dc586829 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 ddefb748ba..0bb6481276 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 class Config { .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 class Config { 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 df185fa35c..066e36dfd4 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 class BisqGrpcServer { 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 @@ public class BisqGrpcServer { .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 13733ef6f4..11aec6a10e 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 @@ import static io.grpc.Status.UNAUTHENTICATED; @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 class TokenAuthInterceptor implements ServerInterceptor { private void authenticate(String authToken) { if (authToken == null) - throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Authentication token is missing")); + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("API token is missing")); - 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) { - String[] pair = token.split(":"); - return pair[0].equals(rpcUser) && pair[1].equals(rpcPassword); + if (!authToken.equals(apiToken)) + throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid API token")); } }