001package com.thetransactioncompany.jsonrpc2.client; 002 003 004import java.io.IOException; 005import java.io.OutputStreamWriter; 006 007import java.net.CookieManager; 008import java.net.CookiePolicy; 009import java.net.HttpCookie; 010import java.net.HttpURLConnection; 011import java.net.URISyntaxException; 012import java.net.URL; 013import java.net.URLConnection; 014 015import java.security.SecureRandom; 016import java.security.cert.X509Certificate; 017 018import java.util.Collections; 019import java.util.List; 020 021import javax.net.ssl.HttpsURLConnection; 022import javax.net.ssl.SSLContext; 023import javax.net.ssl.SSLSocketFactory; 024import javax.net.ssl.TrustManager; 025import javax.net.ssl.X509TrustManager; 026 027import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification; 028import com.thetransactioncompany.jsonrpc2.JSONRPC2ParseException; 029import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; 030import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; 031 032 033/** 034 * Sends requests and / or notifications to a specified JSON-RPC 2.0 server 035 * URL. The JSON-RPC 2.0 messages are dispatched by means of HTTP(S) POST. 036 * This class is thread-safe. 037 * 038 * <p>The client-session class has a number of {@link JSONRPC2SessionOptions 039 * optional settings}. To change them pass a modified options instance to the 040 * {@link #setOptions setOptions()} method. 041 * 042 * <p>Example JSON-RPC 2.0 client session: 043 * 044 * <pre> 045 * // First, import the required packages: 046 * 047 * // The Client sessions package 048 * import com.thetransactioncompany.jsonrpc2.client.*; 049 * 050 * // The Base package for representing JSON-RPC 2.0 messages 051 * import com.thetransactioncompany.jsonrpc2.*; 052 * 053 * // The JSON Smart package for JSON encoding/decoding (optional) 054 * import net.minidev.json.*; 055 * 056 * // For creating URLs 057 * import java.net.*; 058 * 059 * // ... 060 * 061 * 062 * // Creating a new session to a JSON-RPC 2.0 web service at a specified URL 063 * 064 * // The JSON-RPC 2.0 server URL 065 * URL serverURL = null; 066 * 067 * try { 068 * serverURL = new URL("http://jsonrpc.example.com:8080"); 069 * 070 * } catch (MalformedURLException e) { 071 * // handle exception... 072 * } 073 * 074 * // Create new JSON-RPC 2.0 client session 075 * JSONRPC2Session mySession = new JSONRPC2Session(serverURL); 076 * 077 * 078 * // Once the client session object is created, you can use to send a series 079 * // of JSON-RPC 2.0 requests and notifications to it. 080 * 081 * // Sending an example "getServerTime" request: 082 * 083 * // Construct new request 084 * String method = "getServerTime"; 085 * int requestID = 0; 086 * JSONRPC2Request request = new JSONRPC2Request(method, requestID); 087 * 088 * // Send request 089 * JSONRPC2Response response = null; 090 * 091 * try { 092 * response = mySession.send(request); 093 * 094 * } catch (JSONRPC2SessionException e) { 095 * 096 * System.err.println(e.getMessage()); 097 * // handle exception... 098 * } 099 * 100 * // Print response result / error 101 * if (response.indicatesSuccess()) 102 * System.out.println(response.getResult()); 103 * else 104 * System.out.println(response.getError().getMessage()); 105 * 106 * </pre> 107 * 108 * @author Vladimir Dzhuvinov 109 * @author Mike Outland 110 */ 111public 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 final 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 return Collections.emptyList(); 340 } 341 342 return cookieManager.getCookieStore().getCookies(); 343 } 344 345 346 /** 347 * Applies the required headers to the specified URL connection. 348 * 349 * @param con The URL connection which must be open. 350 */ 351 private void applyHeaders(final URLConnection con) { 352 353 // Expect UTF-8 for JSON 354 con.setRequestProperty("Accept-Charset", "UTF-8"); 355 356 // Add "Content-Type" header? 357 if (options.getRequestContentType() != null) 358 con.setRequestProperty("Content-Type", options.getRequestContentType()); 359 360 // Add "Origin" header? 361 if (options.getOrigin() != null) 362 con.setRequestProperty("Origin", options.getOrigin()); 363 364 // Add "Accept-Encoding: gzip, deflate" header? 365 if (options.enableCompression()) 366 con.setRequestProperty("Accept-Encoding", "gzip, deflate"); 367 368 // Add "Cookie" headers? 369 if (options.acceptCookies()) { 370 371 StringBuilder buf = new StringBuilder(); 372 373 for (HttpCookie cookie: getCookies()) { 374 375 if (buf.length() > 0) 376 buf.append("; "); 377 378 buf.append(cookie.toString()); 379 } 380 381 con.setRequestProperty("Cookie", buf.toString()); 382 } 383 } 384 385 386 /** 387 * Creates and configures a new URL connection to the JSON-RPC 2.0 388 * server endpoint according to the session settings. 389 * 390 * @return The URL connection, configured and ready for output (HTTP 391 * POST). 392 * 393 * @throws JSONRPC2SessionException If the URL connection couldn't be 394 * created or configured. 395 */ 396 private URLConnection createURLConnection() 397 throws JSONRPC2SessionException { 398 399 // Open HTTP connection 400 URLConnection con; 401 402 try { 403 // Use proxy? 404 if (options.getProxy() != null) 405 con = url.openConnection(options.getProxy()); 406 else 407 con = url.openConnection(); 408 409 } catch (IOException e) { 410 411 throw new JSONRPC2SessionException( 412 "Network exception: " + e.getMessage(), 413 JSONRPC2SessionException.NETWORK_EXCEPTION, 414 e); 415 } 416 417 con.setConnectTimeout(options.getConnectTimeout()); 418 con.setReadTimeout(options.getReadTimeout()); 419 420 applyHeaders(con); 421 422 // Set POST mode 423 con.setDoOutput(true); 424 425 // Set trust all certs SSL factory? 426 if (con instanceof HttpsURLConnection && options.trustsAllCerts()) { 427 428 if (trustAllSocketFactory == null) { 429 closeStreams(con); 430 throw new JSONRPC2SessionException("Couldn't obtain trust-all SSL socket factory"); 431 } 432 433 ((HttpsURLConnection)con).setSSLSocketFactory(trustAllSocketFactory); 434 } 435 436 // Apply connection configurator? 437 if (connectionConfigurator != null) 438 connectionConfigurator.configure((HttpURLConnection)con); 439 440 return con; 441 } 442 443 444 /** 445 * Posts string data (i.e. JSON string) to the specified URL 446 * connection. 447 * 448 * @param con The URL connection. Must be in HTTP POST mode. Must not 449 * be {@code null}. 450 * @param data The string data to post. Must not be {@code null}. 451 * 452 * @throws JSONRPC2SessionException If an I/O exception is encountered. 453 */ 454 private static void postString(final URLConnection con, final String data) 455 throws JSONRPC2SessionException { 456 457 try { 458 OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream(), "UTF-8"); 459 wr.write(data); 460 wr.flush(); 461 wr.close(); 462 463 } catch (IOException e) { 464 465 throw new JSONRPC2SessionException( 466 "Network exception: " + e.getMessage(), 467 JSONRPC2SessionException.NETWORK_EXCEPTION, 468 e); 469 } 470 } 471 472 473 /** 474 * Reads the raw response from an URL connection (after HTTP POST). 475 * Invokes the {@link RawResponseInspector} if configured and stores 476 * any cookies {@link JSONRPC2SessionOptions#acceptCookies()} if 477 * required}. 478 * 479 * @param con The URL connection. It should contain ready data for 480 * retrieval. Must not be {@code null}. 481 * 482 * @return The raw response. 483 * 484 * @throws JSONRPC2SessionException If an exception is encountered. 485 */ 486 private RawResponse readRawResponse(final URLConnection con) 487 throws JSONRPC2SessionException { 488 489 RawResponse rawResponse; 490 491 try { 492 rawResponse = RawResponse.parse((HttpURLConnection)con); 493 494 } catch (IOException e) { 495 496 throw new JSONRPC2SessionException( 497 "Network exception: " + e.getMessage(), 498 JSONRPC2SessionException.NETWORK_EXCEPTION, 499 e); 500 } 501 502 if (responseInspector != null) 503 responseInspector.inspect(rawResponse); 504 505 if (options.acceptCookies()) { 506 507 // Init cookie manager? 508 if (cookieManager == null) 509 cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL); 510 511 try { 512 cookieManager.put(con.getURL().toURI(), rawResponse.getHeaderFields()); 513 514 } catch (URISyntaxException e) { 515 516 throw new JSONRPC2SessionException( 517 "Network exception: " + e.getMessage(), 518 JSONRPC2SessionException.NETWORK_EXCEPTION, 519 e); 520 521 } catch (IOException e) { 522 523 throw new JSONRPC2SessionException( 524 "I/O exception: " + e.getMessage(), 525 JSONRPC2SessionException.NETWORK_EXCEPTION, 526 e); 527 } 528 } 529 530 return rawResponse; 531 } 532 533 534 /** 535 * Closes the input, output and error streams of the specified URL 536 * connection. No attempt is made to close the underlying socket with 537 * {@code conn.disconnect} so it may be cached (HTTP 1.1 keep live). 538 * See http://techblog.bozho.net/caveats-of-httpurlconnection/ 539 * 540 * @param con The URL connection. May be {@code null}. 541 */ 542 private static void closeStreams(final URLConnection con) { 543 544 if (con == null) { 545 return; 546 } 547 548 try { 549 if (con.getInputStream() != null) { 550 con.getInputStream().close(); 551 } 552 } catch (Exception e) { 553 // ignore 554 } 555 556 try { 557 if (con.getOutputStream() != null) { 558 con.getOutputStream().close(); 559 } 560 } catch (Exception e) { 561 // ignore 562 } 563 564 try { 565 HttpURLConnection httpCon = (HttpURLConnection)con; 566 567 if (httpCon.getErrorStream() != null) { 568 httpCon.getErrorStream().close(); 569 } 570 } catch (Exception e) { 571 // ignore 572 } 573 } 574 575 576 /** 577 * Sends a JSON-RPC 2.0 request using HTTP POST and returns the server 578 * response. 579 * 580 * @param request The JSON-RPC 2.0 request to send. Must not be 581 * {@code null}. 582 * 583 * @return The JSON-RPC 2.0 response returned by the server. 584 * 585 * @throws JSONRPC2SessionException On a network error, unexpected HTTP 586 * response content type or invalid 587 * JSON-RPC 2.0 response. 588 */ 589 public JSONRPC2Response send(final JSONRPC2Request request) 590 throws JSONRPC2SessionException { 591 592 // Create and configure URL connection to server endpoint 593 URLConnection con = createURLConnection(); 594 595 final RawResponse rawResponse; 596 597 try { 598 // Send request encoded as JSON 599 postString(con, request.toString()); 600 601 // Get the response 602 rawResponse = readRawResponse(con); 603 604 } finally { 605 closeStreams(con); 606 } 607 608 // Check response content type 609 String contentType = rawResponse.getContentType(); 610 611 if (! options.isAllowedResponseContentType(contentType)) { 612 613 String msg; 614 615 if (contentType == null) 616 msg = "Missing Content-Type header in the HTTP response"; 617 else 618 msg = "Unexpected \"" + contentType + "\" content type of the HTTP response"; 619 620 throw new JSONRPC2SessionException(msg, JSONRPC2SessionException.UNEXPECTED_CONTENT_TYPE); 621 } 622 623 // Parse and return the response 624 JSONRPC2Response response; 625 626 try { 627 response = JSONRPC2Response.parse(rawResponse.getContent(), 628 options.preservesParseOrder(), 629 options.ignoresVersion(), 630 options.parsesNonStdAttributes()); 631 632 } catch (JSONRPC2ParseException e) { 633 634 throw new JSONRPC2SessionException( 635 "Invalid JSON-RPC 2.0 response", 636 JSONRPC2SessionException.BAD_RESPONSE, 637 e); 638 } 639 640 // Response ID must match the request ID, except for 641 // -32700 (parse error), -32600 (invalid request) and 642 // -32603 (internal error) 643 644 Object reqID = request.getID(); 645 Object resID = response.getID(); 646 647 if (reqID != null && resID != null && reqID.toString().equals(resID.toString()) ) { 648 // ok 649 } else if (reqID == null && resID == null) { 650 // ok 651 } else if (! response.indicatesSuccess() && ( response.getError().getCode() == -32700 || 652 response.getError().getCode() == -32600 || 653 response.getError().getCode() == -32603 )) { 654 // ok 655 } else { 656 throw new JSONRPC2SessionException( 657 "Invalid JSON-RPC 2.0 response: ID mismatch: Returned " + 658 resID + ", expected " + reqID, 659 JSONRPC2SessionException.BAD_RESPONSE); 660 } 661 662 return response; 663 } 664 665 666 /** 667 * Sends a JSON-RPC 2.0 notification using HTTP POST. Note that 668 * contrary to requests, notifications produce no server response. 669 * 670 * @param notification The JSON-RPC 2.0 notification to send. Must not 671 * be {@code null}. 672 * 673 * @throws JSONRPC2SessionException On a network error. 674 */ 675 public void send(final JSONRPC2Notification notification) 676 throws JSONRPC2SessionException { 677 678 // Create and configure URL connection to server endpoint 679 URLConnection con = createURLConnection(); 680 681 // Send notification encoded as JSON 682 try { 683 postString(con, notification.toString()); 684 685 } finally { 686 687 closeStreams(con); 688 } 689 } 690} 691