package org.ripple.power.txns.btc; import java.io.IOException; import java.io.OutputStream; import java.lang.management.ManagementFactory; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.List; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; import org.ripple.power.config.LSystem; import com.sun.net.httpserver.BasicAuthenticator; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.InputStreamReader; import java.lang.management.LockInfo; import java.lang.management.MonitorInfo; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.math.BigInteger; import java.util.logging.Handler; import java.util.logging.Logger; public class RpcHandler implements HttpHandler { /** JSON-RPC error codes */ private static final int RPC_INVALID_REQUEST = -32600; // Invalid request private static final int RPC_METHOD_NOT_FOUND = -32601; // Method not found private static final int RPC_INTERNAL_ERROR = -32603; // Internal server error /** Bitcoin RPC error codes */ private static final int RPC_DATABASE_ERROR = -20; // Database error private static final int RPC_INVALID_PARAMETER = -8; // Invalid parameter private static final int RPC_INVALID_ADDRESS_OR_KEY = -5; // Invalid address or key /** RPC port */ private final int rpcPort; /** Allowed RPC hosts */ private final List<InetAddress> rpcAllowIp; /** RPC user */ private final String rpcUser; /** RPC password */ private final String rpcPassword; /** HTTP server */ private HttpServer server; /** * Create the JSON-RPC request handler * * @param rpcPort RPC port * @param rpcAllowIp List of allowed host addresses * @param rpcUser RPC user * @param rpcPassword RPC password */ public RpcHandler(int rpcPort, List<InetAddress> rpcAllowIp, String rpcUser, String rpcPassword) { this.rpcPort = rpcPort; this.rpcAllowIp = rpcAllowIp; this.rpcUser = rpcUser; this.rpcPassword = rpcPassword; // // Create the HTTP server using a single execution thread // try { server = HttpServer.create(new InetSocketAddress(rpcPort), 10); HttpContext context = server.createContext("/", this); context.setAuthenticator(new RpcAuthenticator(LSystem.applicationName)); server.setExecutor(null); server.start(); BTCLoader.info(String.format("RPC handler started on port %d", rpcPort)); } catch (IOException exc) { BTCLoader.error("Unable to set up HTTP server", exc); } } /** * Shutdowns the RPC request handler */ public void shutdown() { if (server != null) { BTCLoader.info("Stopping RPC handler"); server.stop(5); BTCLoader.info("RPC handler stopped"); } } /** * Handle an HTTP request * * @param exchange HTTP exchange * @throws IOException Error detected while handling the request */ @Override public void handle(HttpExchange exchange) throws IOException { try { int responseCode; String responseBody; // // Get the HTTP request // InetSocketAddress requestAddress = exchange.getRemoteAddress(); String requestMethod = exchange.getRequestMethod(); Headers requestHeaders = exchange.getRequestHeaders(); String contentType = requestHeaders.getFirst("Content-Type"); Headers responseHeaders = exchange.getResponseHeaders(); BTCLoader.debug(String.format("%s request received from %s", requestMethod, requestAddress.getAddress())); if (!rpcAllowIp.contains(requestAddress.getAddress())) { responseCode = HttpURLConnection.HTTP_UNAUTHORIZED; responseBody = "Your IP address is not authorized to access this server"; responseHeaders.set("Content-Type", "text/plain"); } else if (!exchange.getRequestMethod().equals("POST")) { responseCode = HttpURLConnection.HTTP_BAD_METHOD; responseBody = String.format("%s requests are not supported", exchange.getRequestMethod()); responseHeaders.set("Content-Type", "text/plain"); } else if (contentType == null || !contentType.equals("application/json-rpc")) { responseCode = HttpURLConnection.HTTP_BAD_REQUEST; responseBody = "Content type must be application/json-rpc"; responseHeaders.set("Content-Type", "text/plain"); } else { responseBody = processRequest(exchange); responseCode = HttpURLConnection.HTTP_OK; responseHeaders.set("Content-Type", "application/json-rpc"); } // // Return the HTTP response // responseHeaders.set("Cache-Control", "no-cache, no-store, must-revalidate, private"); responseHeaders.set("Server", "JavaBitcoin"); byte[] responseBytes = responseBody.getBytes("UTF-8"); exchange.sendResponseHeaders(responseCode, responseBytes.length); try (OutputStream out = exchange.getResponseBody()) { out.write(responseBytes); } BTCLoader.debug(String.format("RPC request from %s completed", requestAddress.getAddress())); } catch (IOException exc) { BTCLoader.error("Unable to process RPC request", exc); throw exc; } } /** * Handle a JSON-RPC request * * @param exchange HTTP exchange * @throws IOException I/O exception * @return The response in JSON format */ private String processRequest(HttpExchange exchange) throws IOException { String method = ""; Object params = null; Object id = null; Object result = null; int errorCode = 0; String errorMessage = ""; // // Parse the request // try (InputStreamReader in = new InputStreamReader(exchange.getRequestBody(), "UTF-8")) { Object object = new JSONObject(new JSONTokener(in)); if (object == null || !(object instanceof JSONObject)) { errorCode = RPC_INVALID_REQUEST; errorMessage = "The request must be a JSON structured object"; } else { JSONObject request = (JSONObject)object; object = request.get("method"); if (object == null || !(object instanceof String)) { errorCode = RPC_INVALID_REQUEST; errorMessage = "The request must include the 'method' field"; } else { method = (String)object; params = request.get("params"); id = request.get("id"); } } } catch (Exception exc) { errorCode = RPC_INVALID_REQUEST; errorMessage = String.format("Parse exception %s", exc.getMessage()); BTCLoader.error(errorMessage); } catch (Throwable exc) { errorCode = RPC_INTERNAL_ERROR; errorMessage = "Unable to parse request"; BTCLoader.error(errorMessage, exc); } // // Process the request // if (errorCode == 0) { try { switch (method.toLowerCase()) { case "getinfo": result = getInfo(); break; case "getlog": result = getLog(); break; case "getpeerinfo": result = getPeerInfo(); break; case "getblock": result = getBlock(params); break; case "getblockhash": result = getBlockHash(params); break; case "getstacktraces": result = getStackTraces(); break; default: errorCode = RPC_METHOD_NOT_FOUND; errorMessage = String.format("Method '%s' is not recognized", method); } } catch (BlockStoreException exc) { errorCode = RPC_DATABASE_ERROR; errorMessage = "Unable to access database"; } catch (RequestException exc) { errorCode = exc.getCode(); errorMessage = exc.getMessage(); } catch (IllegalArgumentException exc) { errorCode = RPC_INVALID_PARAMETER; errorMessage = exc.getMessage(); } } // // Return the response // JSONObject response = new JSONObject(); if (errorCode != 0) { JSONObject error = new JSONObject(); error.put("code", errorCode); error.put("message", errorMessage); response.put("error", error); } else { response.put("result", result); } if (id != null) response.put("id", id); return response.toString(); } /** * Process 'getinfo' request * * @return Response as a JSONObject */ private JSONObject getInfo() { BTCLoader.debug("Processing 'getinfo'"); JSONObject result = new JSONObject(); // // Get the network difficulty as a Double // BigInteger targetDifficulty = BTCLoader.blockStore.getTargetDifficulty(); double networkDifficulty = (NetParams.PROOF_OF_WORK_LIMIT.divide(targetDifficulty)).doubleValue(); result.put("difficulty", networkDifficulty); // // Get the chain height as an Integer // result.put("blocks", BTCLoader.blockStore.getChainHeight()); // // Get the connection count as an Integer // List<Peer> connectionList = BTCLoader.networkHandler.getConnections(); result.put("connections", connectionList.size()); return result; } /** * Process 'getlog' request * * @return Response as a JSONArray */ private JSONArray getLog() { BTCLoader.debug("Processing 'getlog'"); JSONArray result = new JSONArray(); Logger logger = Logger.getLogger(""); Handler[] handlers = logger.getHandlers(); for (Handler handler : handlers) { if (handler instanceof MemoryLogHandler) { result.put(((MemoryLogHandler)handler).getMessages()); break; } } return result; } /** * Process 'getpeerinfo' request * * @return Response as a JSONArray */ private JSONArray getPeerInfo() { BTCLoader.debug("Processing 'getpeerinfo'"); JSONArray result = new JSONArray(); List<Peer> connectionList = BTCLoader.networkHandler.getConnections(); for(Peer peer:connectionList){ JSONObject peerInfo = new JSONObject(); PeerAddress addr = peer.getAddress(); peerInfo.put("addr", addr.toString()); peerInfo.put("conntime", addr.getTimeConnected()); peerInfo.put("inbound", !addr.isOutbound()); peerInfo.put("version", peer.getVersion()); peerInfo.put("subver", peer.getUserAgent()); peerInfo.put("services", Long.toString(peer.getServices())); peerInfo.put("banscore", peer.getBanScore()); peerInfo.put("startingheight", peer.getHeight()); result.put(peerInfo); } return result; } /** * Process 'getblock' request * * @param params Request parameters * @return Response as a JSONObject * @throws BlockStoreException Unable to get block from database * @throws RequestException Error while processing the request */ private JSONObject getBlock(Object params) throws BlockStoreException, RequestException { if (params == null || !(params instanceof JSONArray) || ((JSONArray)params).length()==0) throw new RequestException(RPC_INVALID_PARAMETER, "The block hash must be specified"); Object elem = ((JSONArray)params).get(0); if (!(elem instanceof String)) throw new RequestException(RPC_INVALID_PARAMETER, "The block hash must be a string"); BTCLoader.debug("Processing 'getblock' for "+(String)elem); JSONObject result = new JSONObject(); StoredBlock storedBlock = BTCLoader.blockStore.getStoredBlock(new Sha256Hash((String)elem)); if (storedBlock == null) throw new RequestException(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); Block block = storedBlock.getBlock(); JSONArray idList = new JSONArray(); List<Transaction> txList = block.getTransactions(); for(Transaction tx:txList){ idList.put(tx.getHashAsString()); } result.put("hash", block.getHashAsString()); result.put("previousblockhash", block.getPrevBlockHash().toString()); result.put("merkleroot", block.getMerkleRoot().toString()); result.put("size", block.getBytes().length); result.put("tx", idList); result.put("time", block.getTimeStamp()); result.put("version", block.getVersion()); result.put("nonce", block.getNonce()); result.put("difficulty", block.getTargetDifficulty()); result.put("height", storedBlock.getHeight()); result.put("chainwork", storedBlock.getChainWork().toString(16)); return result; } /** * Process 'getblockhash' request * * @param params Request parameters * @return Response as a String * @throws BlockStoreException Unable to get block from database * @throws RequestException Error while processing the request */ private String getBlockHash(Object params) throws BlockStoreException, RequestException { if (params == null || !(params instanceof JSONArray) || ((JSONArray)params).length()==0) throw new RequestException(RPC_INVALID_PARAMETER, "The block height must be specified"); Object elem = ((JSONArray)params).get(0); if (!(elem instanceof Long)) throw new RequestException(RPC_INVALID_PARAMETER, "The block height must be an integer"); BTCLoader.debug("Processing 'getblockhash' for "+(Long)elem); Sha256Hash blockHash = BTCLoader.blockStore.getBlockId(((Long)elem).intValue()); if (blockHash == null) throw new RequestException(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); return blockHash.toString(); } /** * Process 'getstacktraces' request * * Response parameters: * locks - An array of lock objects for locks with waiters * threads - An array of thread objects * * Lock object: * name - Lock class name * hash - Lock identity hash code * thread - Identifier of thread holding the lock * * Monitor object: * name - Monitor class name * hash - Monitor identity hash * depth - Stack depth where monitor locked * trace - Stack element where monitor locked * * Thread object: * blocked - Lock object if thread is waiting on a lock * id - Thread identifier * locks - Array of monitor objects for locks held by this thread * name - Thread name * state - Thread state * trace - Array of stack trace elements * * @return Response as a JSONObject */ private JSONObject getStackTraces() { JSONArray threadsJSON = new JSONArray(); JSONArray locksJSON = new JSONArray(); ThreadMXBean tmxBean = ManagementFactory.getThreadMXBean(); boolean tmxMI = tmxBean.isObjectMonitorUsageSupported(); ThreadInfo[] tList = tmxBean.dumpAllThreads(tmxMI, false); // // Generate the response // for (ThreadInfo tInfo : tList) { JSONObject threadJSON = new JSONObject(); // // General thread information // threadJSON.put("id", tInfo.getThreadId()); threadJSON.put("name", tInfo.getThreadName()); threadJSON.put("state", tInfo.getThreadState().toString()); // // Gather lock usage // if (tmxMI) { MonitorInfo[] mList = tInfo.getLockedMonitors(); if (mList.length > 0) { JSONArray monitorsJSON = new JSONArray(); for (MonitorInfo mInfo : mList) { JSONObject lockJSON = new JSONObject(); lockJSON.put("name", mInfo.getClassName()); lockJSON.put("hash", mInfo.getIdentityHashCode()); lockJSON.put("depth", mInfo.getLockedStackDepth()); lockJSON.put("trace", mInfo.getLockedStackFrame().toString()); monitorsJSON.put(lockJSON); } threadJSON.put("locks", monitorsJSON); } if (tInfo.getThreadState() == Thread.State.BLOCKED) { LockInfo lInfo = tInfo.getLockInfo(); if (lInfo != null) { JSONObject lockJSON = new JSONObject(); lockJSON.put("name", lInfo.getClassName()); lockJSON.put("hash", lInfo.getIdentityHashCode()); lockJSON.put("thread", tInfo.getLockOwnerId()); threadJSON.put("blocked", lockJSON); boolean addLock = true; for (int i=0;i<locksJSON.length();i++){ Object lock = locksJSON.get(i); if (((String)((JSONObject)lock).get("name")).equals(lInfo.getClassName())) { addLock = false; break; } } if (addLock) locksJSON.put(lockJSON); } } } // // Add the stack trace // StackTraceElement[] elements = tInfo.getStackTrace(); JSONArray traceJSON = new JSONArray(); for (StackTraceElement element : elements) traceJSON.put(element.toString()); threadJSON.put("trace", traceJSON); // // Add the thread to the response // threadsJSON.put(threadJSON); } // // Return the response // JSONObject response = new JSONObject(); response.put("threads", threadsJSON); response.put("locks", locksJSON); return response; } /** * RPC request authenticator */ private class RpcAuthenticator extends BasicAuthenticator { /** * Crete a Basic Authenticator * * @param realm HTTP realm */ public RpcAuthenticator(String realm) { super(realm); } /** * Check the credentials for the RPC request * * @param user User name * @param password User password */ @Override public boolean checkCredentials(String user, String password) { return (user.equals(rpcUser) && password.equals(rpcPassword)); } } public int getRpcPort() { return rpcPort; } }