2nd attempt at a tiny daonode rest service

Based on chimp1984's https://github.com/bisq-network/bisq/pull/6184
This commit is contained in:
ghubstan 2022-05-07 18:37:42 -03:00
parent cee00f5b9c
commit c70810edd7
No known key found for this signature in database
GPG Key ID: E35592D6800A861E
16 changed files with 938 additions and 0 deletions

View File

@ -103,6 +103,7 @@ configure([project(':cli'),
project(':seednode'),
project(':statsnode'),
project(':pricenode'),
project(':daonode'),
project(':inventory'),
project(':apitest')]) {
@ -686,6 +687,49 @@ configure(project(':seednode')) {
}
}
configure(project(':daonode')) {
apply plugin: 'com.github.johnrengelman.shadow'
mainClassName = 'bisq.daonode.DaoNodeMain'
dependencies {
implementation project(':common')
implementation project(':proto')
implementation project(':p2p')
implementation project(':core')
implementation "com.google.guava:guava:$guavaVersion"
implementation("com.google.inject:guice:$guiceVersion") {
exclude(module: 'guava')
}
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-core:$logbackVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
implementation "com.google.protobuf:protobuf-java-util:$protobufVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testImplementation "org.apache.httpcomponents:httpclient:$httpclientVersion"
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
}
configure(project(':statsnode')) {
mainClassName = 'bisq.statistics.StatisticsMain'

View File

@ -0,0 +1,65 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daonode;
import bisq.core.app.misc.AppSetup;
import bisq.core.app.misc.AppSetupWithP2PAndDAO;
import bisq.core.dao.state.DaoStateService;
import bisq.core.network.p2p.inventory.GetInventoryRequestHandler;
import bisq.core.user.Preferences;
import com.google.inject.Injector;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import bisq.daonode.service.DaoNodeService;
@Slf4j
public class DaoNode {
@Setter
private Injector injector;
private AppSetup appSetup;
private DaoNodeService daoNodeService;
private GetInventoryRequestHandler getInventoryRequestHandler;
public DaoNode() {
}
public void startApplication(int restServerPort) {
appSetup = injector.getInstance(AppSetupWithP2PAndDAO.class);
// todo should run as full dao node when in production
injector.getInstance(Preferences.class).setUseFullModeDaoMonitor(false);
appSetup.start();
getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class);
DaoStateService daoStateService = injector.getInstance(DaoStateService.class);
daoNodeService = new DaoNodeService(daoStateService);
daoNodeService.start(restServerPort);
}
public void shutDown() {
getInventoryRequestHandler.shutDown();
daoNodeService.shutDown();
}
}

View File

@ -0,0 +1,215 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daonode;
import bisq.core.app.TorSetup;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.user.Cookie;
import bisq.core.user.CookieKey;
import bisq.core.user.User;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.P2PServiceListener;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.app.DevEnv;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
import com.google.inject.Key;
import com.google.inject.name.Names;
import lombok.extern.slf4j.Slf4j;
//todo not sure if the restart handling from seed nodes is required
@Slf4j
public class DaoNodeMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final int DEFAULT_REST_SERVER_PORT = 8080;
private static final String VERSION = "1.8.4";
private DaoNode daoNode;
private Timer checkConnectionLossTime;
private int restServerPort = DEFAULT_REST_SERVER_PORT;
public DaoNodeMain() {
super("Bisq Daonode", "bisq-daonode", "bisq_daonode", VERSION);
}
public static void main(String[] args) {
System.out.println("DaoNode.VERSION: " + VERSION);
new DaoNodeMain().execute(args);
}
@Override
protected void doExecute() {
super.doExecute();
checkMemory(config, this);
keepRunning();
}
@Override
protected void addCapabilities() {
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
daoNode = new DaoNode();
onApplicationLaunched();
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new ModuleForAppWithP2p(config);
}
@Override
protected void applyInjector() {
super.applyInjector();
daoNode.setInjector(injector);
if (DevEnv.isDaoActivated()) {
injector.getInstance(DaoStateSnapshotService.class).setDaoRequiresRestartHandler(this::gracefulShutDown);
}
}
@Override
protected void startApplication() {
super.startApplication();
Cookie cookie = injector.getInstance(User.class).getCookie();
cookie.getAsOptionalBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART).ifPresent(wasCleanTorDirSet -> {
if (wasCleanTorDirSet) {
injector.getInstance(TorSetup.class).cleanupTorFiles(() -> {
log.info("Tor directory reset");
cookie.remove(CookieKey.CLEAN_TOR_DIR_AT_RESTART);
}, log::error);
}
});
//todo add program arg for port
daoNode.startApplication(restServerPort);
injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() {
@Override
public void onDataReceived() {
// Do nothing
}
@Override
public void onNoSeedNodeAvailable() {
// Do nothing
}
@Override
public void onNoPeersAvailable() {
// Do nothing
}
@Override
public void onUpdatedDataReceived() {
// Do nothing
}
@Override
public void onTorNodeReady() {
// Do nothing
}
@Override
public void onHiddenServicePublished() {
boolean preventPeriodicShutdownAtSeedNode = injector.getInstance(Key.get(boolean.class,
Names.named(Config.PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE)));
if (!preventPeriodicShutdownAtSeedNode) {
startShutDownInterval(DaoNodeMain.this);
}
UserThread.runAfter(() -> setupConnectionLossCheck(), 60);
}
@Override
public void onSetupFailed(Throwable throwable) {
// Do nothing
}
@Override
public void onRequestCustomBridges() {
// Do nothing
}
});
}
private void setupConnectionLossCheck() {
// For dev testing (usually on BTC_REGTEST) we don't want to get the seed shut
// down as it is normal that the seed is the only actively running node.
if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.BTC_REGTEST) {
return;
}
if (checkConnectionLossTime != null) {
return;
}
checkConnectionLossTime = UserThread.runPeriodically(() -> {
if (injector.getInstance(PeerManager.class).getNumAllConnectionsLostEvents() > 1) {
// We set a flag to clear tor cache files at re-start. We cannot clear it now as Tor is used and
// that can cause problems.
injector.getInstance(User.class).getCookie().putAsBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART, true);
shutDown(this);
}
}, CHECK_CONNECTION_LOSS_SEC);
}
private void gracefulShutDown() {
gracefulShutDown(() -> {
});
}
@Override
public void gracefulShutDown(ResultHandler resultHandler) {
daoNode.shutDown();
super.gracefulShutDown(resultHandler);
}
}

View File

@ -0,0 +1,41 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daonode.dto;
import lombok.Getter;
/**
* Minimal data required for Bisq 2 reputation use case.
* Need to be in sync with the Bisq 2 ProofOfBurnDto class.
*/
@Getter
public class ProofOfBurnDto {
private String txId;
private final long burnedAmount;
private final int blockHeight;
private final long time;
private final String hash;
public ProofOfBurnDto(String txId, long burnedAmount, int blockHeight, long time, String hash) {
this.txId = txId;
this.burnedAmount = burnedAmount;
this.blockHeight = blockHeight;
this.time = time;
this.hash = hash;
}
}

View File

@ -0,0 +1,59 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daonode.service;
import bisq.core.dao.state.DaoStateService;
import lombok.extern.slf4j.Slf4j;
import bisq.daonode.web.WebServer;
import bisq.daonode.web.jdk.JdkServer;
// Todo We should limit usage to localhost as its not intended at that stage to be used
// as a public API, but rather be used by Bisq 2 bridge clients or BSQ explorer nodes,
// both running in a localhost environment. As long that holds, we do not require a high
// level of protection against malicious usage.
// TODO This JDK http server is a super simple implementation. We might use some other
// lightweight REST server framework.
@Slf4j
public class DaoNodeService {
private WebServer webServer;
private DaoStateService daoStateService;
public DaoNodeService(DaoStateService daoStateService) {
this.daoStateService = daoStateService;
}
public void start(int port) {
try {
webServer = new JdkServer(port, daoStateService);
webServer.start();
} catch (Throwable t) {
log.error(t.toString());
}
}
public void shutDown() {
if (webServer != null) {
webServer.stop(0);
}
}
}

View File

@ -0,0 +1,8 @@
package bisq.daonode.web;
public interface WebServer {
void start();
void stop(int delay);
}

View File

@ -0,0 +1,132 @@
package bisq.daonode.web.jdk;
import bisq.core.dao.state.DaoStateService;
import bisq.common.util.Utilities;
import java.net.InetSocketAddress;
import java.io.IOException;
import java.util.concurrent.Executor;
import lombok.extern.slf4j.Slf4j;
import static bisq.daonode.web.jdk.handler.ResourcePathElement.DAONODE;
import bisq.daonode.web.WebServer;
import bisq.daonode.web.jdk.handler.RestHandler;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
// https://dev.to/piczmar_0/framework-less-rest-api-in-java-1jbl
// https://doc.networknt.com/getting-started/light-rest-4j
// https://github.com/piczmar/pure-java-rest-api
// https://stackoverflow.com/questions/3732109/simple-http-server-in-java-using-only-java-se-api
// https://www.programcreek.com/java-api-examples/index.php?api=com.sun.net.httpserver.HttpServer
/**
* From https://stackoverflow.com/questions/3732109/simple-http-server-in-java-using-only-java-se-api
*
* Note that this is, in contrary to what some developers think, absolutely not forbidden
* by the well known FAQ Why Developers Should Not Write Programs That Call 'sun' Packages.
* That FAQ concerns the sun.* package (such as sun.misc.BASE64Encoder) for internal usage
* by the Oracle JRE (which would thus kill your application when you run it on a different
* JRE), not the com.sun.* package. Sun/Oracle also just develop software on top of the
* Java SE API themselves like as every other company such as Apache and so on. Moreover,
* this specific HttpServer must be present in every JDK so there is absolutely no means
* of "portability" issue like as would happen with sun.* package. Using com.sun.* classes
* is only discouraged (but not forbidden) when it concerns an implementation of a certain
* Java API, such as GlassFish (Java EE impl), Mojarra (JSF impl), Jersey (JAX-RS impl), etc.
*/
@Slf4j
public class JdkServer extends HttpServer implements WebServer {
public static void main(String[] args) throws InterruptedException {
WebServer webServer = new JdkServer(8080, null);
webServer.start();
Thread.sleep(40000);
webServer.stop(0);
}
private final int port;
private final DaoStateService daoStateService;
private HttpServer server;
public JdkServer(int port, DaoStateService daoStateService) {
this.port = port;
this.daoStateService = daoStateService;
configure();
}
private void configure() {
try {
this.server = HttpServer.create(new InetSocketAddress(port), 0);
// As use case is intended for a 1 client environment we can stick with a single thread.
setExecutor(Utilities.getSingleThreadExecutor("DaoNode-API"));
// Map all request URLs starting with "/daonode" to a single RestHandler.
// The RestHandler will pass valid requests on to an appropriate handler.
createContext("/" + DAONODE, new RestHandler(daoStateService));
} catch (IOException ex) {
log.error(ex.toString());
throw new RuntimeException(ex);
}
}
@Override
public void bind(InetSocketAddress addr, int backlog) throws IOException {
server.bind(addr, backlog);
}
@Override
public void start() {
server.start();
}
@Override
public void setExecutor(Executor executor) {
server.setExecutor(executor);
}
@Override
public Executor getExecutor() {
return server.getExecutor();
}
@Override
public HttpContext createContext(String path, HttpHandler handler) {
return server.createContext(path, handler);
}
@Override
public HttpContext createContext(String path) {
return server.createContext(path);
}
@Override
public void removeContext(String path) throws IllegalArgumentException {
server.removeContext(path);
}
@Override
public void removeContext(HttpContext context) {
server.removeContext(context);
}
@Override
public InetSocketAddress getAddress() {
return server.getAddress();
}
@Override
public void stop(int delay) {
if (server != null) {
server.stop(0);
}
}
}

View File

@ -0,0 +1,99 @@
package bisq.daonode.web.jdk.handler;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.common.util.Hex;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static bisq.daonode.web.jdk.handler.HandlerUtil.sendResponse;
import static bisq.daonode.web.jdk.handler.HandlerUtil.toJson;
import static bisq.daonode.web.jdk.handler.HandlerUtil.wrapErrorResponse;
import static bisq.daonode.web.jdk.handler.HandlerUtil.wrapResponse;
import static bisq.daonode.web.jdk.handler.ResourcePathElement.BLOCKHEIGHT;
import bisq.daonode.dto.ProofOfBurnDto;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
/**
* Handles daonode/proofofburn requests. Request URLs must match:
* http://localhost:<port>/daonode/proofofburn/blockheight/<blockheight-value>
*
* Example: http://localhost:8080/daonode/proofofburn/blockheight/731270
*/
@Slf4j
class GetProofOfBurnHandler implements HttpHandler {
private final DaoStateService daoStateService;
private final RequestSpec requestSpec;
/**
* A new handler instance must be used for each request. We do not want to parse
* details from each request URI more than once; they are passed to this constructor
* from the RestHandler via the RequestSpec argument.
*
* @param daoStateService DaoStateService singleton
* @param requestSpec RESTful request details, including parsed URL parameters
*/
public GetProofOfBurnHandler(DaoStateService daoStateService, RequestSpec requestSpec) {
this.daoStateService = daoStateService;
this.requestSpec = requestSpec;
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
try {
if (daoStateService == null) {
log.warn("DAO Node daoStateService is null; OK during web server dev/test.");
sendResponse(httpExchange, wrapResponse("[]"));
} else {
int blockHeight = requestSpec.getIntParam(BLOCKHEIGHT);
log.info("Requesting POB for blockheight {}.", blockHeight);
List<ProofOfBurnDto> data = getProofOfBurnDtoList(blockHeight);
if (data != null) {
sendResponse(httpExchange, wrapResponse(toJson(data)));
} else {
log.error("DAO Node Proof of Burn data for blockHeight {} is null.", blockHeight);
sendResponse(500,
httpExchange,
wrapErrorResponse(toJson("DAO Node proof of burn data is null.")));
}
}
} catch (RuntimeException ex) {
sendResponse(500,
httpExchange,
wrapErrorResponse(toJson(ex.getMessage())));
}
}
private List<ProofOfBurnDto> getProofOfBurnDtoList(int fromBlockHeight) {
return daoStateService.getProofOfBurnTxs().stream()
.filter(tx -> tx.getBlockHeight() >= fromBlockHeight)
.map(tx -> new ProofOfBurnDto(tx.getId(),
tx.getBurntBsq(),
tx.getBlockHeight(),
tx.getTime(),
getHash(tx)))
.collect(Collectors.toList());
}
// We strip out the version bytes
private String getHash(Tx tx) {
byte[] opReturnData = tx.getLastTxOutput().getOpReturnData();
if (opReturnData == null) {
return "";
}
return Hex.encode(Arrays.copyOfRange(opReturnData, 2, 22));
}
}

View File

@ -0,0 +1,51 @@
package bisq.daonode.web.jdk.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.OutputStream;
import lombok.SneakyThrows;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.sun.net.httpserver.HttpExchange;
class HandlerUtil {
static void setDefaultResponseHeaders(HttpExchange httpExchange) {
httpExchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
}
static void sendResponse(HttpExchange httpExchange, String response) throws IOException {
sendResponse(200, httpExchange, response);
}
static void sendResponse(int status, HttpExchange httpExchange, String response) throws IOException {
setDefaultResponseHeaders(httpExchange);
byte[] responseBytes = response.getBytes(UTF_8);
httpExchange.sendResponseHeaders(status, responseBytes.length);
OutputStream os = httpExchange.getResponseBody();
os.write(responseBytes);
os.close();
}
// TODO make as function toWhat?
static String wrapResponse(String jsonData) {
return format("{\"data\":%s}", jsonData);
}
// TODO make as function toErrorWhat?
static String wrapErrorResponse(String jsonError) {
return format("{\"error\":%s}", jsonError);
}
@SneakyThrows
static String toJson(Object object) {
return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(object);
}
}

View File

@ -0,0 +1,117 @@
package bisq.daonode.web.jdk.handler;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.lang.System.arraycopy;
import com.sun.net.httpserver.HttpExchange;
/**
* REST request URI parser to extract parameter names and values from an HttpExchange.
*
* Splits the HttpExchange's request URI into a String[] of pathElements identifying a
* (REST) service name, a resource name, and any parameter/value pairs.
*
* This class is limited to URIs adhering a specific pattern:
* pathElements[0] = service-name, e.g., daonode
* pathElements[1] = resource-name, e.g., proofofburn
* pathElements[2, 3...N, N+1] = param-name/value pairs.
*
* For example, request URL http://localhost:8080/daonode/proofofburn/blockheight/731270
* identifies service-name "daonode", resource-name "proofofburn", and one parameter
* "blockheight" with value 731270.
*/
@Getter
@Slf4j
class RequestSpec {
private final HttpExchange httpExchange;
private final String method;
private final URI requestURI;
private final String[] pathElements;
private final String serviceName;
private final String resourceName;
private final Map<String, String> parametersByName;
public RequestSpec(HttpExchange httpExchange) {
this.httpExchange = httpExchange;
this.method = httpExchange.getRequestMethod();
this.requestURI = httpExchange.getRequestURI();
this.pathElements = toPathElements.apply(requestURI);
this.serviceName = pathElements[0];
this.resourceName = pathElements[1];
try {
this.parametersByName = getParametersByName();
} catch (URISyntaxException ex) {
// OK to throw ex in this constructor?
log.error(ex.toString());
throw new IllegalArgumentException(ex.toString());
}
}
public boolean isRequestingResource(String resourceName) {
return this.resourceName.equalsIgnoreCase(resourceName);
}
public String getStringParam(String paramName) {
if (parametersByName.containsKey(paramName))
return parametersByName.get(paramName);
else
throw new IllegalArgumentException(format("Parameter '%s' not found.", paramName));
}
public int getIntParam(String paramName) {
if (parametersByName.containsKey(paramName)) {
var value = parametersByName.get(paramName);
try {
return Integer.parseInt(value);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(
format("Parameter '%s' value '%s' is not a number.",
paramName,
value));
}
} else {
throw new IllegalArgumentException(format("Parameter '%s' not found.", paramName));
}
}
private final Function<URI, String[]> toPathElements = (uri) -> {
String[] raw = uri.getPath().split("/");
String[] elements = new String[raw.length - 1];
arraycopy(raw, 1, elements, 0, elements.length);
return elements;
};
private Map<String, String> getParametersByName() throws URISyntaxException {
Map<String, String> params = new HashMap<>();
if (pathElements.length == 2)
return params; // There are no parameter name/value pairs in url.
// All pathElements beyond index 1 should be param-name/value pairs, and
// a param-value must follow each param-name.
Predicate<Integer> paramValueExists = (i) -> (i + 1) < pathElements.length;
for (int i = 2; i < pathElements.length; i++) {
String name = pathElements[i];
if (paramValueExists.test(i))
params.put(name, pathElements[++i]);
else
throw new URISyntaxException(requestURI.getPath(),
format("No value found for parameter with name '%s'.", name),
-1);
}
return params;
}
}

View File

@ -0,0 +1,7 @@
package bisq.daonode.web.jdk.handler;
public class ResourcePathElement {
public static String DAONODE = "daonode";
public static final String BLOCKHEIGHT = "blockheight";
public static final String PROOFOFBURN = "proofofburn";
}

View File

@ -0,0 +1,61 @@
package bisq.daonode.web.jdk.handler;
import bisq.core.dao.state.DaoStateService;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import static bisq.daonode.web.jdk.handler.HandlerUtil.sendResponse;
import static bisq.daonode.web.jdk.handler.HandlerUtil.toJson;
import static bisq.daonode.web.jdk.handler.HandlerUtil.wrapErrorResponse;
import static bisq.daonode.web.jdk.handler.ResourcePathElement.PROOFOFBURN;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
/**
* All HTTP requests are routed to a singleton RestHandler, then passed on to
* appropriate sub handler instances.
*/
@Slf4j
public class RestHandler implements HttpHandler {
private final DaoStateService daoStateService;
public RestHandler(DaoStateService daoStateService) {
this.daoStateService = daoStateService;
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
try {
if (httpExchange == null)
throw new NullPointerException("HttpExchange cannot be null.");
// Parse the request URI details here, and pass them to a new
// sub handler instance.
RequestSpec requestSpec = new RequestSpec(httpExchange);
// Atm we only use GET, no plans yet for allow POST for DaoNode API.
if (!requestSpec.getMethod().equals("GET")) {
sendResponse(405,
httpExchange,
wrapErrorResponse(toJson("Forbidden HTTP method " + requestSpec.getMethod())));
} else if (requestSpec.isRequestingResource(PROOFOFBURN)) {
new GetProofOfBurnHandler(daoStateService, requestSpec).handle(httpExchange);
} else {
sendResponse(404, httpExchange, wrapErrorResponse(toJson("Not Found")));
}
} catch (RuntimeException ex) {
sendResponse(500,
httpExchange,
wrapErrorResponse(toJson(ex.getMessage())));
} finally {
httpExchange.close();
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="com.msopentech.thali.toronionproxy.OnionProxyManagerEventHandler" level="INFO"/>
</configuration>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
The :daemon & :cli jars contain their own logback.xml config files, which causes chatty logback startup.
To avoid chatty logback msgs during its configuration, pass logback.configurationFile as a system property:
-Dlogback.configurationFile=apitest/build/resources/main/logback.xml
The gradle build file takes care of adding this system property to the bisq-apitest script.
-->
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="io.grpc.netty" level="WARN"/>
</configuration>

View File

@ -3,3 +3,6 @@ systemProp.org.gradle.internal.http.socketTimeout=120000
# Makes Y/N user prompts more readable during installer packaging
org.gradle.console=plain
# org.gradle.dependency.verification.console=verbose
org.gradle.dependency.verification=lenient

View File

@ -11,6 +11,7 @@ include 'pricenode'
include 'relay'
include 'seednode'
include 'statsnode'
include 'daonode'
include 'inventory'
include 'apitest'