001 package com.thetransactioncompany.jsonrpc2.client; 002 003 004 import java.io.IOException; 005 import java.io.OutputStreamWriter; 006 007 import java.net.CookieManager; 008 import java.net.CookiePolicy; 009 import java.net.HttpCookie; 010 import java.net.HttpURLConnection; 011 import java.net.URISyntaxException; 012 import java.net.URL; 013 import java.net.URLConnection; 014 015 import java.security.SecureRandom; 016 017 import java.security.cert.X509Certificate; 018 019 import java.util.Collections; 020 import java.util.List; 021 022 import javax.net.ssl.HttpsURLConnection; 023 import javax.net.ssl.SSLContext; 024 import javax.net.ssl.SSLSocketFactory; 025 import javax.net.ssl.TrustManager; 026 import javax.net.ssl.X509TrustManager; 027 028 import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification; 029 import com.thetransactioncompany.jsonrpc2.JSONRPC2ParseException; 030 import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; 031 import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; 032 033 034 /** 035 * Sends requests and / or notifications to a specified JSON-RPC 2.0 server 036 * URL. The JSON-RPC 2.0 messages are dispatched by means of HTTP(S) POST. 037 * This class is thread-safe. 038 * 039 * <p>The client-session class has a number of {@link JSONRPC2SessionOptions 040 * optional settings}. To change them pass a modified options instance to the 041 * {@link #setOptions setOptions()} method. 042 * 043 * <p>Example JSON-RPC 2.0 client session: 044 * 045 * <pre> 046 * // First, import the required packages: 047 * 048 * // The Client sessions package 049 * import com.thetransactioncompany.jsonrpc2.client.*; 050 * 051 * // The Base package for representing JSON-RPC 2.0 messages 052 * import com.thetransactioncompany.jsonrpc2.*; 053 * 054 * // The JSON Smart package for JSON encoding/decoding (optional) 055 * import net.minidev.json.*; 056 * 057 * // For creating URLs 058 * import java.net.*; 059 * 060 * // ... 061 * 062 * 063 * // Creating a new session to a JSON-RPC 2.0 web service at a specified URL 064 * 065 * // The JSON-RPC 2.0 server URL 066 * URL serverURL = null; 067 * 068 * try { 069 * serverURL = new URL("http://jsonrpc.example.com:8080"); 070 * 071 * } catch (MalformedURLException e) { 072 * // handle exception... 073 * } 074 * 075 * // Create new JSON-RPC 2.0 client session 076 * JSONRPC2Session mySession = new JSONRPC2Session(serverURL); 077 * 078 * 079 * // Once the client session object is created, you can use to send a series 080 * // of JSON-RPC 2.0 requests and notifications to it. 081 * 082 * // Sending an example "getServerTime" request: 083 * 084 * // Construct new request 085 * String method = "getServerTime"; 086 * int requestID = 0; 087 * JSONRPC2Request request = new JSONRPC2Request(method, requestID); 088 * 089 * // Send request 090 * JSONRPC2Response response = null; 091 * 092 * try { 093 * response = mySession.send(request); 094 * 095 * } catch (JSONRPC2SessionException e) { 096 * 097 * System.err.println(e.getMessage()); 098 * // handle exception... 099 * } 100 * 101 * // Print response result / error 102 * if (response.indicatesSuccess()) 103 * System.out.println(response.getResult()); 104 * else 105 * System.out.println(response.getError().getMessage()); 106 * 107 * </pre> 108 * 109 * @author Vladimir Dzhuvinov 110 */ 111 public class JSONRPC2Session { 112 113 114 /** 115 * The server URL, which protocol must be HTTP or HTTPS. 116 * 117 * <p>Example URL: "http://jsonrpc.example.com:8080" 118 */ 119 private URL url; 120 121 122 /** 123 * The client-session options. 124 */ 125 private JSONRPC2SessionOptions options; 126 127 128 /** 129 * Custom HTTP URL connection configurator. 130 */ 131 private ConnectionConfigurator connectionConfigurator; 132 133 134 /** 135 * Optional HTTP raw response inspector. 136 */ 137 private RawResponseInspector responseInspector; 138 139 140 /** 141 * Optional HTTP cookie manager. 142 */ 143 private CookieManager cookieManager; 144 145 146 /** 147 * Trust-all-certs (including self-signed) SSL socket factory. 148 */ 149 private static SSLSocketFactory trustAllSocketFactory = createTrustAllSocketFactory(); 150 151 152 /** 153 * Creates a new client session to a JSON-RPC 2.0 server at the 154 * specified URL. Uses a default {@link JSONRPC2SessionOptions} 155 * instance. 156 * 157 * @param url The server URL, e.g. "http://jsonrpc.example.com:8080". 158 * Must not be {@code null}. 159 */ 160 public JSONRPC2Session(final URL url) { 161 162 if (! url.getProtocol().equalsIgnoreCase("http") && 163 ! url.getProtocol().equalsIgnoreCase("https") ) 164 throw new IllegalArgumentException("The URL protocol must be HTTP or HTTPS"); 165 166 this.url = url; 167 168 // Default session options 169 options = new JSONRPC2SessionOptions(); 170 171 // No initial connection configurator 172 connectionConfigurator = null; 173 } 174 175 176 /** 177 * Creates a trust-all-certificates SSL socket factory. Encountered 178 * exceptions are not rethrown. 179 * 180 * @return The SSL socket factory. 181 */ 182 public static SSLSocketFactory createTrustAllSocketFactory() { 183 184 TrustManager[] trustAllCerts = new TrustManager[] { 185 186 new X509TrustManager() { 187 188 public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } 189 190 public void checkClientTrusted(X509Certificate[] certs, String authType) { } 191 192 public void checkServerTrusted(X509Certificate[] certs, String authType) { } 193 } 194 }; 195 196 try { 197 SSLContext sc = SSLContext.getInstance("SSL"); 198 sc.init(null, trustAllCerts, new SecureRandom()); 199 return sc.getSocketFactory(); 200 201 } catch (Exception e) { 202 203 // Ignore 204 return null; 205 } 206 } 207 208 209 /** 210 * Gets the JSON-RPC 2.0 server URL. 211 * 212 * @return The server URL. 213 */ 214 public URL getURL() { 215 216 return url; 217 } 218 219 220 /** 221 * Sets the JSON-RPC 2.0 server URL. 222 * 223 * @param url The server URL. Must not be {@code null}. 224 */ 225 public void setURL(final URL url) { 226 227 if (url == null) 228 throw new IllegalArgumentException("The server URL must not be null"); 229 230 this.url = url; 231 } 232 233 234 /** 235 * Gets the JSON-RPC 2.0 client session options. 236 * 237 * @return The client session options. 238 */ 239 public JSONRPC2SessionOptions getOptions() { 240 241 return options; 242 } 243 244 245 /** 246 * Sets the JSON-RPC 2.0 client session options. 247 * 248 * @param options The client session options, must not be {@code null}. 249 */ 250 public void setOptions(final JSONRPC2SessionOptions options) { 251 252 if (options == null) 253 throw new IllegalArgumentException("The client session options must not be null"); 254 255 this.options = options; 256 } 257 258 259 /** 260 * Gets the custom HTTP URL connection configurator. 261 * 262 * @since 1.5 263 * 264 * @return The connection configurator, {@code null} if none is set. 265 */ 266 public ConnectionConfigurator getConnectionConfigurator() { 267 268 return connectionConfigurator; 269 } 270 271 272 /** 273 * Specifies a custom HTTP URL connection configurator. It will be 274 * {@link ConnectionConfigurator#configure applied} to each new HTTP 275 * connection after the {@link JSONRPC2SessionOptions session options} 276 * are applied and before the connection is established. 277 * 278 * <p>This method may be used to set custom HTTP request headers, 279 * timeouts or other properties. 280 * 281 * @since 1.5 282 * 283 * @param connectionConfigurator A custom HTTP URL connection 284 * configurator, {@code null} to remove 285 * a previously set one. 286 */ 287 public void setConnectionConfigurator(final ConnectionConfigurator connectionConfigurator) { 288 289 this.connectionConfigurator = connectionConfigurator; 290 } 291 292 293 /** 294 * Gets the optional inspector for the raw HTTP responses. 295 * 296 * @since 1.6 297 * 298 * @return The optional inspector for the raw HTTP responses, 299 * {@code null} if none is set. 300 */ 301 public RawResponseInspector getRawResponseInspector() { 302 303 return responseInspector; 304 } 305 306 307 /** 308 * Specifies an optional inspector for the raw HTTP responses to 309 * JSON-RPC 2.0 requests and notifications. Its 310 * {@link RawResponseInspector#inspect inspect} method will be called 311 * upon reception of a HTTP response. 312 * 313 * <p>You can use the {@link RawResponseInspector} interface to 314 * retrieve the unparsed response content and headers. 315 * 316 * @since 1.6 317 * 318 * @param responseInspector An optional inspector for the raw HTTP 319 * responses, {@code null} to remove a 320 * previously set one. 321 */ 322 public void setRawResponseInspector(final RawResponseInspector responseInspector) { 323 324 this.responseInspector = responseInspector; 325 } 326 327 328 /** 329 * Gets all non-expired HTTP cookies currently stored in the client. 330 * 331 * @return The HTTP cookies, or empty list if none were set by the 332 * server or cookies are not 333 * {@link JSONRPC2SessionOptions#acceptCookies accepted}. 334 */ 335 public List<HttpCookie> getCookies() { 336 337 if (cookieManager == null) { 338 339 List<HttpCookie> emptyList = Collections.emptyList(); 340 return emptyList; 341 } 342 343 return cookieManager.getCookieStore().getCookies(); 344 } 345 346 347 /** 348 * Applies the required headers to the specified URL connection. 349 * 350 * @param con The URL connection which must be open. 351 * 352 * @throws JSONRPC2SessionException If an exception is encountered. 353 */ 354 private void applyHeaders(final URLConnection con) 355 throws JSONRPC2SessionException { 356 357 // Expect UTF-8 for JSON 358 con.setRequestProperty("Accept-Charset", "UTF-8"); 359 360 // Add "Content-Type" header? 361 if (options.getRequestContentType() != null) 362 con.setRequestProperty("Content-Type", options.getRequestContentType()); 363 364 // Add "Origin" header? 365 if (options.getOrigin() != null) 366 con.setRequestProperty("Origin", options.getOrigin()); 367 368 // Add "Accept-Encoding: gzip, deflate" header? 369 if (options.enableCompression()) 370 con.setRequestProperty("Accept-Encoding", "gzip, deflate"); 371 372 // Add "Cookie" headers? 373 if (options.acceptCookies()) { 374 375 StringBuilder buf = new StringBuilder(); 376 377 for (HttpCookie cookie: getCookies()) { 378 379 if (buf.length() > 0) 380 buf.append("; "); 381 382 buf.append(cookie.toString()); 383 } 384 385 con.setRequestProperty("Cookie", buf.toString()); 386 } 387 } 388 389 390 /** 391 * Creates and configures a new URL connection to the JSON-RPC 2.0 392 * server endpoint according to the session settings. 393 * 394 * @return The URL connection, configured and ready for output (HTTP 395 * POST). 396 * 397 * @throws JSONRPC2SessionException If the URL connection couldn't be 398 * created or configured. 399 */ 400 private URLConnection createURLConnection() 401 throws JSONRPC2SessionException { 402 403 // Open HTTP connection 404 URLConnection con = null; 405 406 try { 407 // Use proxy? 408 if (options.getProxy() != null) 409 con = url.openConnection(options.getProxy()); 410 else 411 con = url.openConnection(); 412 413 } catch (IOException e) { 414 415 throw new JSONRPC2SessionException( 416 "Network exception: " + e.getMessage(), 417 JSONRPC2SessionException.NETWORK_EXCEPTION, 418 e); 419 } 420 421 con.setConnectTimeout(options.getConnectTimeout()); 422 con.setReadTimeout(options.getReadTimeout()); 423 424 applyHeaders(con); 425 426 // Set POST mode 427 con.setDoOutput(true); 428 429 // Set trust all certs SSL factory? 430 if (con instanceof HttpsURLConnection && options.trustsAllCerts()) { 431 432 if (trustAllSocketFactory == null) 433 throw new JSONRPC2SessionException("Couldn't obtain trust-all SSL socket factory"); 434 435 ((HttpsURLConnection)con).setSSLSocketFactory(trustAllSocketFactory); 436 } 437 438 // Apply connection configurator? 439 if (connectionConfigurator != null) 440 connectionConfigurator.configure((HttpURLConnection)con); 441 442 return con; 443 } 444 445 446 /** 447 * Posts string data (i.e. JSON string) to the specified URL 448 * connection. 449 * 450 * @param con The URL connection. Must be in HTTP POST mode. Must not 451 * be {@code null}. 452 * @param data The string data to post. Must not be {@code null}. 453 * 454 * @throws JSONRPC2SessionException If an I/O exception is encountered. 455 */ 456 private static void postString(final URLConnection con, final String data) 457 throws JSONRPC2SessionException { 458 459 try { 460 OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream(), "UTF-8"); 461 wr.write(data); 462 wr.flush(); 463 wr.close(); 464 465 } catch (IOException e) { 466 467 throw new JSONRPC2SessionException( 468 "Network exception: " + e.getMessage(), 469 JSONRPC2SessionException.NETWORK_EXCEPTION, 470 e); 471 } 472 } 473 474 475 /** 476 * Reads the raw response from an URL connection (after HTTP POST). 477 * Invokes the {@link RawResponseInspector} if configured and stores 478 * any cookies {@link JSONRPC2SessionOptions#storeCookies if required}. 479 * 480 * @param con The URL connection. It should contain ready data for 481 * retrieval. Must not be {@code null}. 482 * 483 * @return The raw response. 484 * 485 * @throws JSONRPC2SessionException If an exception is encountered. 486 */ 487 private RawResponse readRawResponse(final URLConnection con) 488 throws JSONRPC2SessionException { 489 490 RawResponse rawResponse = null; 491 492 try { 493 rawResponse = RawResponse.parse((HttpURLConnection)con); 494 495 } catch (IOException e) { 496 497 throw new JSONRPC2SessionException( 498 "Network exception: " + e.getMessage(), 499 JSONRPC2SessionException.NETWORK_EXCEPTION, 500 e); 501 } 502 503 if (responseInspector != null) 504 responseInspector.inspect(rawResponse); 505 506 if (options.acceptCookies()) { 507 508 // Init cookie manager? 509 if (cookieManager == null) 510 cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL); 511 512 try { 513 cookieManager.put(con.getURL().toURI(), rawResponse.getHeaderFields()); 514 515 } catch (URISyntaxException e) { 516 517 throw new JSONRPC2SessionException( 518 "Network exception: " + e.getMessage(), 519 JSONRPC2SessionException.NETWORK_EXCEPTION, 520 e); 521 522 } catch (IOException e) { 523 524 throw new JSONRPC2SessionException( 525 "I/O exception: " + e.getMessage(), 526 JSONRPC2SessionException.NETWORK_EXCEPTION, 527 e); 528 } 529 } 530 531 return rawResponse; 532 } 533 534 535 /** 536 * Sends a JSON-RPC 2.0 request using HTTP POST and returns the server 537 * response. 538 * 539 * @param request The JSON-RPC 2.0 request to send. Must not be 540 * {@code null}. 541 * 542 * @return The JSON-RPC 2.0 response returned by the server. 543 * 544 * @throws JSONRPC2SessionException On a network error, unexpected HTTP 545 * response content type or invalid 546 * JSON-RPC 2.0 response. 547 */ 548 public JSONRPC2Response send(final JSONRPC2Request request) 549 throws JSONRPC2SessionException { 550 551 // Create and configure URL connection to server endpoint 552 URLConnection con = createURLConnection(); 553 554 // Send request encoded as JSON 555 postString(con, request.toString()); 556 557 // Get the response 558 RawResponse rawResponse = readRawResponse(con); 559 560 // Check response content type 561 String contentType = rawResponse.getContentType(); 562 563 if (! options.isAllowedResponseContentType(contentType)) { 564 565 String msg = null; 566 567 if (contentType == null) 568 msg = "Missing Content-Type header in the HTTP response"; 569 else 570 msg = "Unexpected \"" + contentType + "\" content type of the HTTP response"; 571 572 throw new JSONRPC2SessionException(msg, JSONRPC2SessionException.UNEXPECTED_CONTENT_TYPE); 573 } 574 575 // Parse and return the response 576 JSONRPC2Response response = null; 577 578 try { 579 response = JSONRPC2Response.parse(rawResponse.getContent(), 580 options.preservesParseOrder(), 581 options.ignoresVersion(), 582 options.parsesNonStdAttributes()); 583 584 } catch (JSONRPC2ParseException e) { 585 586 throw new JSONRPC2SessionException( 587 "Invalid JSON-RPC 2.0 response", 588 JSONRPC2SessionException.BAD_RESPONSE, 589 e); 590 } 591 592 // Response ID must match the request ID, except for 593 // -32700 (parse error), -32600 (invalid request) and 594 // -32603 (internal error) 595 596 Object reqID = request.getID(); 597 Object resID = response.getID(); 598 599 if (reqID != null && resID != null && reqID.toString().equals(resID.toString()) ) { 600 // ok 601 } else if (reqID == null && resID == null) { 602 // ok 603 } else if (! response.indicatesSuccess() && ( response.getError().getCode() == -32700 || 604 response.getError().getCode() == -32600 || 605 response.getError().getCode() == -32603 )) { 606 // ok 607 } else { 608 throw new JSONRPC2SessionException( 609 "Invalid JSON-RPC 2.0 response: ID mismatch: Returned " + 610 resID + ", expected " + reqID, 611 JSONRPC2SessionException.BAD_RESPONSE); 612 } 613 614 return response; 615 } 616 617 618 /** 619 * Sends a JSON-RPC 2.0 notification using HTTP POST. Note that 620 * contrary to requests, notifications produce no server response. 621 * 622 * @param notification The JSON-RPC 2.0 notification to send. Must not 623 * be {@code null}. 624 * 625 * @throws JSONRPC2SessionException On a network error. 626 */ 627 public void send(final JSONRPC2Notification notification) 628 throws JSONRPC2SessionException { 629 630 // Create and configure URL connection to server endpoint 631 URLConnection con = createURLConnection(); 632 633 // Send notification encoded as JSON 634 postString(con, notification.toString()); 635 636 // Get the response /for the inspector only/ 637 RawResponse rawResponse = readRawResponse(con); 638 } 639 } 640