package com.msgilligan.bitcoinj.rpc;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.msgilligan.bitcoinj.rpc.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
/**
* = JSON-RPC Client
*
* This is a concrete class with basic JSON-RPC functionality. In theory it could be used to implement
* other JSON-RPC clients, but as this is a Bitcoin-focused project you probably want to look at
* {@link BitcoinClient} and its subclasses.
*
* This client uses strongly-typed POJOs representing {@link JsonRpcRequest} and {@link JsonRpcResponse}. The
* response object uses a type parameter to specify the object that is the actual JSON-RPC `result`.
* Early versions of this client were http://c2.com/cgi/wiki?StringlyTyped[stringly-typed], but
* these strong types allows us to use Jackson to deserialize
* directly to strongly-typed POJO's without using intermediate `Map` or `JsonNode` types.
*
*/
public class RPCClient extends AbstractRPCClient {
private static final Logger log = LoggerFactory.getLogger(RPCClient.class);
private URI serverURI;
private String username;
private String password;
private static final boolean disableSslVerification = false;
static {
if (disableSslVerification) {
// Disable checks that prevent using a self-signed SSL certificate
// TODO: Should checks be enabled by default for security reasons?
disableSslVerification();
}
}
/**
* Construct a JSON-RPC client from URI, username, and password
*
* Typically you'll want to use {@link BitcoinClient} or one of its subclasses
* @param server server URI should not contain username/password
* @param rpcuser username for the RPC HTTP connection
* @param rpcpassword password for the RPC HTTP connection
*/
public RPCClient(URI server, final String rpcuser, final String rpcpassword) {
super();
this.serverURI = server;
this.username = rpcuser;
this.password = rpcpassword;
}
/**
* Get the URI of the server this client connects to
* @return Server URI
*/
@Override
public URI getServerURI() {
return serverURI;
}
/**
* Send a JSON-RPC request to the server and return a JSON-RPC response.
*
* @param request JSON-RPC request
* @param responseType Response type to deserialize to
* @return JSON-RPC response
* @throws IOException when thrown by the underlying HttpURLConnection
* @throws JsonRPCStatusException when the HTTP response code is other than 200
*/
@Override
protected <R> JsonRpcResponse<R> send(JsonRpcRequest request, JavaType responseType) throws IOException, JsonRPCStatusException {
HttpURLConnection connection = openConnection();
// TODO: Make sure HTTP keep-alive will work
// See: http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-keepalive.html
// http://developer.android.com/reference/java/net/HttpURLConnection.html
// http://android-developers.blogspot.com/2011/09/androids-http-clients.html
if (log.isDebugEnabled()) {
log.debug("Req json: {}", mapper.writeValueAsString(request));
}
OutputStream requestStream = connection.getOutputStream();
mapper.writeValue(requestStream, request);
requestStream.close();
int responseCode = connection.getResponseCode();
log.debug("Response code: {}", responseCode);
if (responseCode != 200) {
handleBadResponseCode(responseCode, connection);
}
JsonRpcResponse<R> responseJson;
try {
if (log.isDebugEnabled()) {
// If logging enabled, copy InputStream to string and log
String responseBody = convertStreamToString(connection.getInputStream());
log.debug("responseBody: {}", responseBody);
responseJson = mapper.readValue(responseBody, responseType);
} else {
// Otherwise convert directly to responseType
responseJson = mapper.readValue(connection.getInputStream(), responseType);
}
} catch (JsonProcessingException e) {
log.error("JsonProcessingException: {}", e);
// TODO: Map to some kind of JsonRPC exception similar to JsonRPCStatusException
throw e;
}
log.debug("Resp json: {}", responseJson);
connection.disconnect();
return responseJson;
}
// Prepare and throw JsonRPCStatusException with all relevant info
private void handleBadResponseCode(int responseCode, HttpURLConnection connection) throws IOException, JsonRPCStatusException {
String responseMessage = connection.getResponseMessage();
String exceptionMessage = responseMessage;
int jsonRPCCode = 0;
JsonRpcResponse bodyJson = null; // Body as JSON if available
String bodyString = null; // Body as String if not JSON
InputStream errorStream = connection.getErrorStream();
if (connection.getContentType().equals("application/json")) {
// We got a JSON error response, parse it
bodyJson = mapper.readValue(errorStream, JsonRpcResponse.class);
JsonRpcError error = bodyJson.getError();
if (error != null) {
// If there's a more specific message in the JSON use it instead.
exceptionMessage = error.getMessage();
jsonRPCCode = error.getCode();
log.error("json error code: {}, message: {}", jsonRPCCode, exceptionMessage);
}
} else {
// No JSON, read response body as string
bodyString = convertStreamToString(errorStream);
log.error("error string: {}", bodyString);
errorStream.close();
}
throw new JsonRPCStatusException(exceptionMessage, responseCode, responseMessage, jsonRPCCode, bodyString, bodyJson);
}
private static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is,"UTF-8").useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
private HttpURLConnection openConnection() throws IOException {
HttpURLConnection connection = (HttpURLConnection) serverURI.toURL().openConnection();
connection.setDoOutput(true); // For writes
connection.setRequestMethod("POST");
// connection.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.toString());
// connection.setRequestProperty("Content-Type", " application/json;charset=" + StandardCharsets.UTF_8.toString());
connection.setRequestProperty("Accept-Charset", "UTF-8");
connection.setRequestProperty("Content-Type", "application/json;charset=" + "UTF-8");
connection.setRequestProperty("Connection", "close"); // Avoid EOFException: http://stackoverflow.com/questions/19641374/android-eofexception-when-using-httpurlconnection-headers
String auth = username + ":" + password;
String basicAuth = "Basic " + Base64.encodeToString(auth.getBytes(),Base64.DEFAULT).trim();
connection.setRequestProperty ("Authorization", basicAuth);
return connection;
}
// TODO: Allow for self-signed certificates without disabling all verification
private static void disableSslVerification() {
try
{
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
// Install the all-trusting trust manager
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// Create all-trusting host name verifier
HostnameVerifier allHostsValid = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
// Install the all-trusting host verifier
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
} catch (NoSuchAlgorithmException | KeyManagementException e ) {
log.error("Exception in disableSslVerification{}", e);
e.printStackTrace();
}
}
}