mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-08-20 04:08:09 -04:00
Add API functions to initialize Haveno account (#216)
Co-authored-by: woodser@protonmail.com
This commit is contained in:
parent
dc4692d97a
commit
e3b9a9962b
81 changed files with 2755 additions and 1660 deletions
64
daemon/src/main/java/bisq/daemon/app/ConsoleInput.java
Normal file
64
daemon/src/main/java/bisq/daemon/app/ConsoleInput.java
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.daemon.app;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* A cancellable console input reader.
|
||||
* Derived from https://www.javaspecialists.eu/archive/Issue153-Timeout-on-Console-Input.html
|
||||
*/
|
||||
public class ConsoleInput {
|
||||
private final int tries;
|
||||
private final int timeout;
|
||||
private final TimeUnit unit;
|
||||
private Future<String> future;
|
||||
|
||||
public ConsoleInput(int tries, int timeout, TimeUnit unit) {
|
||||
this.tries = tries;
|
||||
this.timeout = timeout;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (future != null)
|
||||
future.cancel(true);
|
||||
}
|
||||
|
||||
public String readLine() throws InterruptedException {
|
||||
ExecutorService ex = Executors.newSingleThreadExecutor();
|
||||
String input = null;
|
||||
try {
|
||||
for (int i = 0; i < tries; i++) {
|
||||
future = ex.submit(new ConsoleInputReadTask());
|
||||
try {
|
||||
input = future.get(timeout, unit);
|
||||
break;
|
||||
} catch (ExecutionException e) {
|
||||
e.getCause().printStackTrace();
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
} finally {
|
||||
future = null;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ex.shutdownNow();
|
||||
}
|
||||
return input;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.daemon.app;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ConsoleInputReadTask implements Callable<String> {
|
||||
public String call() throws IOException {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
|
||||
log.debug("ConsoleInputReadTask run() called.");
|
||||
String input;
|
||||
do {
|
||||
try {
|
||||
// wait until we have data to complete a readLine()
|
||||
while (!br.ready()) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
// readline will always block until an input exists.
|
||||
input = br.readLine();
|
||||
} catch (InterruptedException e) {
|
||||
log.debug("ConsoleInputReadTask() cancelled");
|
||||
return null;
|
||||
}
|
||||
} while ("".equals(input));
|
||||
return input;
|
||||
}
|
||||
}
|
|
@ -19,21 +19,24 @@ package bisq.daemon.app;
|
|||
|
||||
import bisq.core.app.HavenoHeadlessAppMain;
|
||||
import bisq.core.app.HavenoSetup;
|
||||
import bisq.core.api.AccountServiceListener;
|
||||
import bisq.core.app.CoreModule;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.AppModule;
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
import java.io.Console;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import bisq.daemon.grpc.GrpcServer;
|
||||
|
||||
@Slf4j
|
||||
|
@ -61,7 +64,6 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
|
|||
@Override
|
||||
protected void launchApplication() {
|
||||
headlessApp = new HavenoDaemon();
|
||||
|
||||
UserThread.execute(this::onApplicationLaunched);
|
||||
}
|
||||
|
||||
|
@ -101,15 +103,116 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
|
|||
@Override
|
||||
protected void onApplicationStarted() {
|
||||
super.onApplicationStarted();
|
||||
|
||||
grpcServer = injector.getInstance(GrpcServer.class);
|
||||
grpcServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gracefulShutDown(ResultHandler resultHandler) {
|
||||
super.gracefulShutDown(resultHandler);
|
||||
if (grpcServer != null) grpcServer.shutdown(); // could be null if application attempted to shutdown early
|
||||
}
|
||||
|
||||
grpcServer.shutdown();
|
||||
/**
|
||||
* Start the grpcServer to allow logging in remotely.
|
||||
*/
|
||||
@Override
|
||||
protected boolean loginAccount() {
|
||||
boolean opened = super.loginAccount();
|
||||
|
||||
// Start rpc server in case login is coming in from rpc
|
||||
grpcServer = injector.getInstance(GrpcServer.class);
|
||||
grpcServer.start();
|
||||
|
||||
if (!opened) {
|
||||
// Nonblocking, we need to stop if the login occurred through rpc.
|
||||
// TODO: add a mode to mask password
|
||||
ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
|
||||
Thread t = new Thread(() -> {
|
||||
interactiveLogin(reader);
|
||||
});
|
||||
t.start();
|
||||
|
||||
// Handle asynchronous account opens.
|
||||
// Will need to also close and reopen account.
|
||||
AccountServiceListener accountListener = new AccountServiceListener() {
|
||||
@Override public void onAccountCreated() { onLogin(); }
|
||||
@Override public void onAccountOpened() { onLogin(); }
|
||||
private void onLogin() {
|
||||
log.info("Logged in successfully");
|
||||
reader.cancel(); // closing the reader will stop all read attempts and end the interactive login thread
|
||||
}
|
||||
};
|
||||
accountService.addListener(accountListener);
|
||||
|
||||
try {
|
||||
// Wait until interactive login or rpc. Check one more time if account is open to close race condition.
|
||||
if (!accountService.isAccountOpen()) {
|
||||
log.info("Interactive login required");
|
||||
t.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
accountService.removeListener(accountListener);
|
||||
opened = accountService.isAccountOpen();
|
||||
}
|
||||
|
||||
return opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks user for login. TODO: Implement in the desktop app.
|
||||
* @return True if user logged in interactively.
|
||||
*/
|
||||
protected boolean interactiveLogin(ConsoleInput reader) {
|
||||
Console console = System.console();
|
||||
if (console == null) {
|
||||
// The ConsoleInput class reads from system.in, can wait for input without a console.
|
||||
log.info("No console available, account must be opened through rpc");
|
||||
try {
|
||||
// If user logs in through rpc, the reader will be interrupted through the event.
|
||||
reader.readLine();
|
||||
} catch (InterruptedException | CancellationException ex) {
|
||||
log.info("Reader interrupted, continuing startup");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String openedOrCreated = "Account unlocked\n";
|
||||
boolean accountExists = accountService.accountExists();
|
||||
while (!accountService.isAccountOpen()) {
|
||||
try {
|
||||
if (accountExists) {
|
||||
try {
|
||||
// readPassword will not return until the user inputs something
|
||||
// which is not suitable if we are waiting for rpc call which
|
||||
// could login the account. Must be able to interrupt the read.
|
||||
//new String(console.readPassword("Password:"));
|
||||
System.out.printf("Password:\n");
|
||||
String password = reader.readLine();
|
||||
accountService.openAccount(password);
|
||||
} catch (IncorrectPasswordException ipe) {
|
||||
System.out.printf("Incorrect password\n");
|
||||
}
|
||||
} else {
|
||||
System.out.printf("Creating a new account\n");
|
||||
System.out.printf("Password:\n");
|
||||
String password = reader.readLine();
|
||||
System.out.printf("Confirm:\n");
|
||||
String passwordConfirm = reader.readLine();
|
||||
if (password.equals(passwordConfirm)) {
|
||||
accountService.createAccount(password);
|
||||
openedOrCreated = "Account created\n";
|
||||
} else {
|
||||
System.out.printf("Passwords did not match\n");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.debug(ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
System.out.printf(openedOrCreated);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
286
daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java
Normal file
286
daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java
Normal file
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.daemon.grpc;
|
||||
|
||||
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
|
||||
import static bisq.proto.grpc.AccountGrpc.getAccountExistsMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getBackupAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getChangePasswordMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getCloseAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getCreateAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getDeleteAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getIsAccountOpenMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getOpenAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getRestoreAccountMethod;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.core.api.CoreApi;
|
||||
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
|
||||
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
|
||||
import bisq.proto.grpc.AccountExistsReply;
|
||||
import bisq.proto.grpc.AccountExistsRequest;
|
||||
import bisq.proto.grpc.AccountGrpc.AccountImplBase;
|
||||
import bisq.proto.grpc.BackupAccountReply;
|
||||
import bisq.proto.grpc.BackupAccountRequest;
|
||||
import bisq.proto.grpc.ChangePasswordReply;
|
||||
import bisq.proto.grpc.ChangePasswordRequest;
|
||||
import bisq.proto.grpc.CloseAccountReply;
|
||||
import bisq.proto.grpc.CloseAccountRequest;
|
||||
import bisq.proto.grpc.CreateAccountReply;
|
||||
import bisq.proto.grpc.CreateAccountRequest;
|
||||
import bisq.proto.grpc.DeleteAccountReply;
|
||||
import bisq.proto.grpc.DeleteAccountRequest;
|
||||
import bisq.proto.grpc.IsAccountOpenReply;
|
||||
import bisq.proto.grpc.IsAccountOpenRequest;
|
||||
import bisq.proto.grpc.IsAppInitializedReply;
|
||||
import bisq.proto.grpc.IsAppInitializedRequest;
|
||||
import bisq.proto.grpc.OpenAccountReply;
|
||||
import bisq.proto.grpc.OpenAccountRequest;
|
||||
import bisq.proto.grpc.RestoreAccountReply;
|
||||
import bisq.proto.grpc.RestoreAccountRequest;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@VisibleForTesting
|
||||
@Slf4j
|
||||
public class GrpcAccountService extends AccountImplBase {
|
||||
|
||||
private final CoreApi coreApi;
|
||||
private final GrpcExceptionHandler exceptionHandler;
|
||||
|
||||
private ByteArrayOutputStream restoreStream; // in memory stream for restoring account
|
||||
|
||||
@Inject
|
||||
public GrpcAccountService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||
this.coreApi = coreApi;
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accountExists(AccountExistsRequest req, StreamObserver<AccountExistsReply> responseObserver) {
|
||||
try {
|
||||
var reply = AccountExistsReply.newBuilder()
|
||||
.setAccountExists(coreApi.accountExists())
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isAccountOpen(IsAccountOpenRequest req, StreamObserver<IsAccountOpenReply> responseObserver) {
|
||||
try {
|
||||
var reply = IsAccountOpenReply.newBuilder()
|
||||
.setIsAccountOpen(coreApi.isAccountOpen())
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAccount(CreateAccountRequest req, StreamObserver<CreateAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.createAccount(req.getPassword());
|
||||
var reply = CreateAccountReply.newBuilder()
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openAccount(OpenAccountRequest req, StreamObserver<OpenAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.openAccount(req.getPassword());
|
||||
var reply = OpenAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
if (cause instanceof IncorrectPasswordException) cause = new IllegalStateException(cause);
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isAppInitialized(IsAppInitializedRequest req, StreamObserver<IsAppInitializedReply> responseObserver) {
|
||||
try {
|
||||
var reply = IsAppInitializedReply.newBuilder().setIsAppInitialized(coreApi.isAppInitialized()).build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(ChangePasswordRequest req, StreamObserver<ChangePasswordReply> responseObserver) {
|
||||
try {
|
||||
coreApi.changePassword(req.getPassword());
|
||||
var reply = ChangePasswordReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeAccount(CloseAccountRequest req, StreamObserver<CloseAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.closeAccount();
|
||||
var reply = CloseAccountReply.newBuilder()
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAccount(DeleteAccountRequest req, StreamObserver<DeleteAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.deleteAccount(() -> {
|
||||
var reply = DeleteAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted(); // reply after shutdown
|
||||
});
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backupAccount(BackupAccountRequest req, StreamObserver<BackupAccountReply> responseObserver) {
|
||||
|
||||
// Send in large chunks to reduce unnecessary overhead. Typical backup will not be more than a few MB.
|
||||
// From current testing it appears that client gRPC-web is slow in processing the bytes on download.
|
||||
try {
|
||||
int bufferSize = 1024 * 1024 * 8;
|
||||
coreApi.backupAccount(bufferSize, (stream) -> {
|
||||
try {
|
||||
log.info("Sending bytes in chunks of: " + bufferSize);
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int length;
|
||||
int total = 0;
|
||||
while ((length = stream.read(buffer, 0, bufferSize)) != -1) {
|
||||
total += length;
|
||||
var reply = BackupAccountReply.newBuilder()
|
||||
.setZipBytes(ByteString.copyFrom(buffer, 0, length))
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
}
|
||||
log.info("Completed backup account total sent: " + total);
|
||||
stream.close();
|
||||
responseObserver.onCompleted();
|
||||
} catch (Exception ex) {
|
||||
exceptionHandler.handleException(log, ex, responseObserver);
|
||||
}
|
||||
}, (ex) -> exceptionHandler.handleException(log, ex, responseObserver));
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreAccount(RestoreAccountRequest req, StreamObserver<RestoreAccountReply> responseObserver) {
|
||||
try {
|
||||
// Fail fast since uploading and processing bytes takes resources.
|
||||
if (coreApi.accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
|
||||
|
||||
// If the entire zip is in memory, no need to write to disk.
|
||||
// Restore the account directly from the zip stream.
|
||||
if (!req.getHasMore() && req.getOffset() == 0) {
|
||||
var inputStream = req.getZipBytes().newInput();
|
||||
coreApi.restoreAccount(inputStream, 1024 * 64, () -> {
|
||||
var reply = RestoreAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted(); // reply after shutdown
|
||||
});
|
||||
} else {
|
||||
if (req.getOffset() == 0) {
|
||||
log.info("RestoreAccount starting new chunked zip");
|
||||
restoreStream = new ByteArrayOutputStream((int) req.getTotalLength());
|
||||
}
|
||||
if (restoreStream.size() != req.getOffset()) {
|
||||
log.warn("Stream offset doesn't match current position");
|
||||
IllegalStateException cause = new IllegalStateException("Stream offset doesn't match current position");
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
} else {
|
||||
log.info("RestoreAccount writing chunk size " + req.getZipBytes().size());
|
||||
req.getZipBytes().writeTo(restoreStream);
|
||||
}
|
||||
|
||||
if (!req.getHasMore()) {
|
||||
var inputStream = new ByteArrayInputStream(restoreStream.toByteArray());
|
||||
restoreStream.close();
|
||||
restoreStream = null;
|
||||
coreApi.restoreAccount(inputStream, 1024 * 64, () -> {
|
||||
var reply = RestoreAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted(); // reply after shutdown
|
||||
});
|
||||
} else {
|
||||
var reply = RestoreAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
}
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
final ServerInterceptor[] interceptors() {
|
||||
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
|
||||
return rateMeteringInterceptor.map(serverInterceptor ->
|
||||
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
|
||||
}
|
||||
|
||||
final Optional<ServerInterceptor> rateMeteringInterceptor() {
|
||||
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
||||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||
new HashMap<>() {{
|
||||
put(getAccountExistsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getBackupAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS));
|
||||
put(getChangePasswordMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getCloseAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getCreateAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getDeleteAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getIsAccountOpenMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getOpenAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getRestoreAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS));
|
||||
}}
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -42,9 +42,9 @@ import bisq.proto.grpc.StartCheckingConnectionsReply;
|
|||
import bisq.proto.grpc.StartCheckingConnectionsRequest;
|
||||
import bisq.proto.grpc.StopCheckingConnectionsReply;
|
||||
import bisq.proto.grpc.StopCheckingConnectionsRequest;
|
||||
import bisq.proto.grpc.UriConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import bisq.proto.grpc.UrlConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
@ -84,7 +84,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
public void removeConnection(RemoveConnectionRequest request,
|
||||
StreamObserver<RemoveConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
coreApi.removeMoneroConnection(validateUri(request.getUri()));
|
||||
coreApi.removeMoneroConnection(validateUri(request.getUrl()));
|
||||
return RemoveConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
public void getConnection(GetConnectionRequest request,
|
||||
StreamObserver<GetConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
UriConnection replyConnection = toUriConnection(coreApi.getMoneroConnection());
|
||||
UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection());
|
||||
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
|
@ -107,8 +107,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
StreamObserver<GetConnectionsReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
List<MoneroRpcConnection> connections = coreApi.getMoneroConnections();
|
||||
List<UriConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
|
||||
List<UrlConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList());
|
||||
return GetConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
|
||||
});
|
||||
}
|
||||
|
@ -117,8 +117,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
public void setConnection(SetConnectionRequest request,
|
||||
StreamObserver<SetConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
if (request.getUri() != null && !request.getUri().isEmpty())
|
||||
coreApi.setMoneroConnection(validateUri(request.getUri()));
|
||||
if (request.getUrl() != null && !request.getUrl().isEmpty())
|
||||
coreApi.setMoneroConnection(validateUri(request.getUrl()));
|
||||
else if (request.hasConnection())
|
||||
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection()));
|
||||
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client
|
||||
|
@ -131,7 +131,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
StreamObserver<CheckConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
MoneroRpcConnection connection = coreApi.checkMoneroConnection();
|
||||
UriConnection replyConnection = toUriConnection(connection);
|
||||
UrlConnection replyConnection = toUrlConnection(connection);
|
||||
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
|
@ -145,8 +145,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
StreamObserver<CheckConnectionsReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
List<MoneroRpcConnection> connections = coreApi.checkMoneroConnections();
|
||||
List<UriConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
|
||||
List<UrlConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList());
|
||||
return CheckConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
|
||||
});
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection();
|
||||
UriConnection replyConnection = toUriConnection(connection);
|
||||
UrlConnection replyConnection = toUrlConnection(connection);
|
||||
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
|
@ -211,43 +211,40 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
|||
}
|
||||
|
||||
|
||||
private static UriConnection toUriConnection(MoneroRpcConnection rpcConnection) {
|
||||
private static UrlConnection toUrlConnection(MoneroRpcConnection rpcConnection) {
|
||||
if (rpcConnection == null) return null;
|
||||
return UriConnection.newBuilder()
|
||||
.setUri(rpcConnection.getUri())
|
||||
return UrlConnection.newBuilder()
|
||||
.setUrl(rpcConnection.getUri())
|
||||
.setPriority(rpcConnection.getPriority())
|
||||
.setOnlineStatus(toOnlineStatus(rpcConnection.isOnline()))
|
||||
.setAuthenticationStatus(toAuthenticationStatus(rpcConnection.isAuthenticated()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static UriConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) {
|
||||
if (authenticated == null) return UriConnection.AuthenticationStatus.NO_AUTHENTICATION;
|
||||
else if (authenticated) return UriConnection.AuthenticationStatus.AUTHENTICATED;
|
||||
else return UriConnection.AuthenticationStatus.NOT_AUTHENTICATED;
|
||||
private static UrlConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) {
|
||||
if (authenticated == null) return UrlConnection.AuthenticationStatus.NO_AUTHENTICATION;
|
||||
else if (authenticated) return UrlConnection.AuthenticationStatus.AUTHENTICATED;
|
||||
else return UrlConnection.AuthenticationStatus.NOT_AUTHENTICATED;
|
||||
}
|
||||
|
||||
private static UriConnection.OnlineStatus toOnlineStatus(Boolean online) {
|
||||
if (online == null) return UriConnection.OnlineStatus.UNKNOWN;
|
||||
else if (online) return UriConnection.OnlineStatus.ONLINE;
|
||||
else return UriConnection.OnlineStatus.OFFLINE;
|
||||
private static UrlConnection.OnlineStatus toOnlineStatus(Boolean online) {
|
||||
if (online == null) return UrlConnection.OnlineStatus.UNKNOWN;
|
||||
else if (online) return UrlConnection.OnlineStatus.ONLINE;
|
||||
else return UrlConnection.OnlineStatus.OFFLINE;
|
||||
}
|
||||
|
||||
private static MoneroRpcConnection toMoneroRpcConnection(UriConnection uriConnection) throws URISyntaxException {
|
||||
private static MoneroRpcConnection toMoneroRpcConnection(UrlConnection uriConnection) throws MalformedURLException {
|
||||
if (uriConnection == null) return null;
|
||||
return new MoneroRpcConnection(
|
||||
validateUri(uriConnection.getUri()),
|
||||
validateUri(uriConnection.getUrl()),
|
||||
nullIfEmpty(uriConnection.getUsername()),
|
||||
nullIfEmpty(uriConnection.getPassword()))
|
||||
.setPriority(uriConnection.getPriority());
|
||||
}
|
||||
|
||||
private static String validateUri(String uri) throws URISyntaxException {
|
||||
if (uri.isEmpty()) {
|
||||
throw new IllegalArgumentException("URI is required");
|
||||
}
|
||||
// Create new URI for validation, internally String is used again
|
||||
return new URI(uri).toString();
|
||||
private static String validateUri(String url) throws MalformedURLException {
|
||||
if (url.isEmpty()) throw new IllegalArgumentException("URL is required");
|
||||
return new URL(url).toString(); // validate and return
|
||||
}
|
||||
|
||||
private static String nullIfEmpty(String value) {
|
||||
|
|
|
@ -49,6 +49,7 @@ public class GrpcServer {
|
|||
public GrpcServer(CoreContext coreContext,
|
||||
Config config,
|
||||
PasswordAuthInterceptor passwordAuthInterceptor,
|
||||
GrpcAccountService accountService,
|
||||
GrpcDisputeAgentsService disputeAgentsService,
|
||||
GrpcHelpService helpService,
|
||||
GrpcOffersService offersService,
|
||||
|
@ -63,6 +64,7 @@ public class GrpcServer {
|
|||
GrpcMoneroConnectionsService moneroConnectionsService) {
|
||||
this.server = ServerBuilder.forPort(config.apiPort)
|
||||
.executor(UserThread.getExecutor())
|
||||
.addService(interceptForward(accountService, accountService.interceptors()))
|
||||
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
||||
.addService(interceptForward(helpService, helpService.interceptors()))
|
||||
.addService(interceptForward(offersService, offersService.interceptors()))
|
||||
|
|
|
@ -48,9 +48,14 @@ class GrpcShutdownService extends ShutdownServerGrpc.ShutdownServerImplBase {
|
|||
StreamObserver<StopReply> responseObserver) {
|
||||
try {
|
||||
log.info("Shutdown request received.");
|
||||
var reply = StopReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
HavenoHeadlessApp.setOnGracefulShutDownHandler(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
var reply = StopReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
});
|
||||
UserThread.runAfter(HavenoHeadlessApp.getShutDownHandler(), 500, MILLISECONDS);
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue