001package org.consensusj.jsonrpc; 002 003import com.fasterxml.jackson.core.JsonProcessingException; 004import com.fasterxml.jackson.databind.JavaType; 005import com.fasterxml.jackson.databind.ObjectMapper; 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009import javax.net.ssl.HttpsURLConnection; 010import javax.net.ssl.SSLContext; 011import javax.net.ssl.SSLSocketFactory; 012import java.io.IOException; 013import java.io.InputStream; 014import java.io.OutputStream; 015import java.lang.reflect.Type; 016import java.net.HttpURLConnection; 017import java.net.URI; 018import java.nio.charset.StandardCharsets; 019import java.util.concurrent.CompletableFuture; 020 021/** 022 * JSON-RPC Client using {@link HttpURLConnection} formerly named{@code RpcClient}. 023 * <p> 024 * This is a concrete class with generic JSON-RPC functionality, it implements the abstract 025 * method {@link JsonRpcClient#sendRequestForResponseAsync(JsonRpcRequest, Type)} using {@link HttpURLConnection}. 026 * <p> 027 * Uses strongly-typed POJOs representing {@link JsonRpcRequest} and {@link JsonRpcResponse}. The 028 * response object uses a parameterized type for the object that is the actual JSON-RPC `result`. 029 * Using strong types and Jackson to serialize/deserialize to/from strongly-typed POJO's without 030 * using intermediate `Map` or `JsonNode` types. 031 * 032 */ 033public class JsonRpcClientHttpUrlConnection implements JsonRpcTransport<JavaType> { 034 private static final Logger log = LoggerFactory.getLogger(JsonRpcClientHttpUrlConnection.class); 035 private final ObjectMapper mapper; 036 private final URI serverURI; 037 private final String username; 038 private final String password; 039 private static final String UTF8 = StandardCharsets.UTF_8.name(); 040 private final SSLSocketFactory sslSocketFactory; 041 042 public JsonRpcClientHttpUrlConnection(ObjectMapper mapper, SSLContext sslContext, URI server, final String rpcUser, final String rpcPassword) { 043 this.mapper = mapper; 044 this.sslSocketFactory = sslContext.getSocketFactory(); 045 log.debug("Constructing JSON-RPC client for: {}", server); 046 this.serverURI = server; 047 this.username = rpcUser; 048 this.password = rpcPassword; 049 } 050 051 /** 052 * Get the URI of the server this client connects to 053 * @return Server URI 054 */ 055 @Override 056 public URI getServerURI() { 057 return serverURI; 058 } 059 060 /** 061 * Send a JSON-RPC request to the server and return a JSON-RPC response. 062 * 063 * @param request JSON-RPC request 064 * @param responseType Response type to deserialize to 065 * @return JSON-RPC response 066 * @throws IOException when thrown by the underlying HttpURLConnection 067 * @throws JsonRpcStatusException when the HTTP response code is other than 200 068 */ 069 @Override 070 public <R> JsonRpcResponse<R> sendRequestForResponse(JsonRpcRequest request, JavaType responseType) throws IOException, JsonRpcStatusException { 071 HttpURLConnection connection = openConnection(); 072 073 // TODO: Make sure HTTP keep-alive will work 074 // See: http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-keepalive.html 075 // http://developer.android.com/reference/java/net/HttpURLConnection.html 076 // http://android-developers.blogspot.com/2011/09/androids-http-clients.html 077 078 if (log.isDebugEnabled()) { 079 log.debug("JsonRpcRequest: {}", mapper.writeValueAsString(request)); 080 } 081 082 try (OutputStream requestStream = connection.getOutputStream()) { 083 mapper.writeValue(requestStream, request); 084 } 085 086 int responseCode = connection.getResponseCode(); 087 log.debug("HTTP Response code: {}", responseCode); 088 089 if (responseCode != 200) { 090 handleBadResponseCode(responseCode, connection); 091 } 092 093 JsonRpcResponse<R> responseJson = responseFromStream(connection.getInputStream(), responseType); 094 connection.disconnect(); 095 return responseJson; 096 } 097 098 @Override 099 public <R> CompletableFuture<JsonRpcResponse<R>> sendRequestForResponseAsync(JsonRpcRequest request, JavaType responseType) { 100 return supplyAsync(() -> this.sendRequestForResponse(request, responseType)); 101 } 102 103 private <R> JsonRpcResponse<R> responseFromStream(InputStream inputStream, JavaType responseType) throws IOException { 104 JsonRpcResponse<R> responseJson; 105 try { 106 if (log.isDebugEnabled()) { 107 // If logging enabled, copy InputStream to string and log 108 String responseBody = convertStreamToString(inputStream); 109 log.debug("Response Body: {}", responseBody); 110 responseJson = mapper.readValue(responseBody, responseType); 111 } else { 112 // Otherwise convert directly to responseType 113 responseJson = mapper.readValue(inputStream, responseType); 114 } 115 } catch (JsonProcessingException e) { 116 log.error("JsonProcessingException: ", e); 117 // TODO: Map to some kind of JsonRPC exception similar to JsonRPCStatusException 118 throw e; 119 } 120 return responseJson; 121 } 122 123 /** 124 * Prepare and throw JsonRPCStatusException with all relevant info 125 * @param responseCode Non-success response code 126 * @param connection the current connection 127 * @throws IOException IO Error 128 * @throws JsonRpcStatusException An exception containing the HTTP status code and a message 129 */ 130 private void handleBadResponseCode(int responseCode, HttpURLConnection connection) throws IOException, JsonRpcStatusException 131 { 132 String responseMessage = connection.getResponseMessage(); // HTTP/1 message like "OK" or "Not found" 133 InputStream errorStream = connection.getErrorStream(); 134 if (errorStream != null) { 135 if (connection.getContentType().equals("application/json")) { 136 // We got a JSON error response -- try to parse it as a JsonRpcResponse 137 JsonRpcResponse<Object> bodyJson = responseFromStream(errorStream, responseTypeFor(Object.class)); 138 // Since this is an error at the JSON level, let's log it with `debug` level and 139 // let the higher-level software decide whether to log it as `error` or not. 140 log.debug("json error code: {}, message: {}", bodyJson.getError().getCode(), responseMessage); 141 throw new JsonRpcStatusException(responseCode, bodyJson); 142 } else { 143 // No JSON, read response body as string 144 String bodyString = convertStreamToString(errorStream); 145 log.error("error string: {}", bodyString); 146 errorStream.close(); 147 throw new JsonRpcStatusException(responseCode, bodyString); 148 } 149 } 150 throw new JsonRpcStatusException(responseMessage, responseCode, responseMessage, 0, null, null); 151 } 152 153 private static String convertStreamToString(java.io.InputStream is) { 154 java.util.Scanner s = new java.util.Scanner(is, UTF8).useDelimiter("\\A"); 155 return s.hasNext() ? s.next() : ""; 156 } 157 158 private HttpURLConnection openConnection() throws IOException { 159 HttpURLConnection connection = (HttpURLConnection) serverURI.toURL().openConnection(); 160 if (connection instanceof HttpsURLConnection) { 161 ((HttpsURLConnection) connection).setSSLSocketFactory(this.sslSocketFactory); 162 } 163 connection.setDoOutput(true); // For writes 164 connection.setRequestMethod("POST"); 165 connection.setRequestProperty("Accept-Charset", UTF8); 166 connection.setRequestProperty("Content-Type", "application/json;charset=" + UTF8); 167 connection.setRequestProperty("Connection", "close"); // Avoid EOFException: http://stackoverflow.com/questions/19641374/android-eofexception-when-using-httpurlconnection-headers 168 169 String auth = username + ":" + password; 170 String basicAuth = "Basic " + JsonRpcTransport.base64Encode(auth); 171 connection.setRequestProperty ("Authorization", basicAuth); 172 173 return connection; 174 } 175 176 private JavaType responseTypeFor(Class<?> resultType) { 177 return mapper.getTypeFactory(). 178 constructParametricType(JsonRpcResponse.class, resultType); 179 } 180}