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; 016 017import java.security.cert.X509Certificate; 018 019import java.util.Collections; 020import java.util.List; 021 022import javax.net.ssl.HttpsURLConnection; 023import javax.net.ssl.SSLContext; 024import javax.net.ssl.SSLSocketFactory; 025import javax.net.ssl.TrustManager; 026import javax.net.ssl.X509TrustManager; 027 028import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification; 029import com.thetransactioncompany.jsonrpc2.JSONRPC2ParseException; 030import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; 031import 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 * @author Mike Outland 111 */ 112public class JSONRPC2Session { 113 114 115 /** 116 * The server URL, which protocol must be HTTP or HTTPS. 117 * 118 * <p>Example URL: "http://jsonrpc.example.com:8080" 119 */ 120 private URL url; 121 122 123 /** 124 * The client-session options. 125 */ 126 private JSONRPC2SessionOptions options; 127 128 129 /** 130 * Custom HTTP URL connection configurator. 131 */ 132 private ConnectionConfigurator connectionConfigurator; 133 134 135 /** 136 * Optional HTTP raw response inspector. 137 */ 138 private RawResponseInspector responseInspector; 139 140 141 /** 142 * Optional HTTP cookie manager. 143 */ 144 private CookieManager cookieManager; 145 146 147 /** 148 * Trust-all-certs (including self-signed) SSL socket factory. 149 */ 150 private static final SSLSocketFactory trustAllSocketFactory = createTrustAllSocketFactory(); 151 152 153 /** 154 * Creates a new client session to a JSON-RPC 2.0 server at the 155 * specified URL. Uses a default {@link JSONRPC2SessionOptions} 156 * instance. 157 * 158 * @param url The server URL, e.g. "http://jsonrpc.example.com:8080". 159 * Must not be {@code null}. 160 */ 161 public JSONRPC2Session(final URL url) { 162 163 if (! url.getProtocol().equalsIgnoreCase("http") && 164 ! url.getProtocol().equalsIgnoreCase("https") ) 165 throw new IllegalArgumentException("The URL protocol must be HTTP or HTTPS"); 166 167 this.url = url; 168 169 // Default session options 170 options = new JSONRPC2SessionOptions(); 171 172 // No initial connection configurator 173 connectionConfigurator = null; 174 } 175 176 177 /** 178 * Creates a trust-all-certificates SSL socket factory. Encountered 179 * exceptions are not rethrown. 180 * 181 * @return The SSL socket factory. 182 */ 183 public static SSLSocketFactory createTrustAllSocketFactory() { 184 185 TrustManager[] trustAllCerts = new TrustManager[] { 186 187 new X509TrustManager() { 188 189 public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } 190 191 public void checkClientTrusted(X509Certificate[] certs, String authType) { } 192 193 public void checkServerTrusted(X509Certificate[] certs, String authType) { } 194 } 195 }; 196 197 try { 198 SSLContext sc = SSLContext.getInstance("SSL"); 199 sc.init(null, trustAllCerts, new SecureRandom()); 200 return sc.getSocketFactory(); 201 202 } catch (Exception e) { 203 204 // Ignore 205 return null; 206 } 207 } 208 209 210 /** 211 * Gets the JSON-RPC 2.0 server URL. 212 * 213 * @return The server URL. 214 */ 215 public URL getURL() { 216 217 return url; 218 } 219 220 221 /** 222 * Sets the JSON-RPC 2.0 server URL. 223 * 224 * @param url The server URL. Must not be {@code null}. 225 */ 226 public void setURL(final URL url) { 227 228 if (url == null) 229 throw new IllegalArgumentException("The server URL must not be null"); 230 231 this.url = url; 232 } 233 234 235 /** 236 * Gets the JSON-RPC 2.0 client session options. 237 * 238 * @return The client session options. 239 */ 240 public JSONRPC2SessionOptions getOptions() { 241 242 return options; 243 } 244 245 246 /** 247 * Sets the JSON-RPC 2.0 client session options. 248 * 249 * @param options The client session options, must not be {@code null}. 250 */ 251 public void setOptions(final JSONRPC2SessionOptions options) { 252 253 if (options == null) 254 throw new IllegalArgumentException("The client session options must not be null"); 255 256 this.options = options; 257 } 258 259 260 /** 261 * Gets the custom HTTP URL connection configurator. 262 * 263 * @since 1.5 264 * 265 * @return The connection configurator, {@code null} if none is set. 266 */ 267 public ConnectionConfigurator getConnectionConfigurator() { 268 269 return connectionConfigurator; 270 } 271 272 273 /** 274 * Specifies a custom HTTP URL connection configurator. It will be 275 * {@link ConnectionConfigurator#configure applied} to each new HTTP 276 * connection after the {@link JSONRPC2SessionOptions session options} 277 * are applied and before the connection is established. 278 * 279 * <p>This method may be used to set custom HTTP request headers, 280 * timeouts or other properties. 281 * 282 * @since 1.5 283 * 284 * @param connectionConfigurator A custom HTTP URL connection 285 * configurator, {@code null} to remove 286 * a previously set one. 287 */ 288 public void setConnectionConfigurator(final ConnectionConfigurator connectionConfigurator) { 289 290 this.connectionConfigurator = connectionConfigurator; 291 } 292 293 294 /** 295 * Gets the optional inspector for the raw HTTP responses. 296 * 297 * @since 1.6 298 * 299 * @return The optional inspector for the raw HTTP responses, 300 * {@code null} if none is set. 301 */ 302 public RawResponseInspector getRawResponseInspector() { 303 304 return responseInspector; 305 } 306 307 308 /** 309 * Specifies an optional inspector for the raw HTTP responses to 310 * JSON-RPC 2.0 requests and notifications. Its 311 * {@link RawResponseInspector#inspect inspect} method will be called 312 * upon reception of a HTTP response. 313 * 314 * <p>You can use the {@link RawResponseInspector} interface to 315 * retrieve the unparsed response content and headers. 316 * 317 * @since 1.6 318 * 319 * @param responseInspector An optional inspector for the raw HTTP 320 * responses, {@code null} to remove a 321 * previously set one. 322 */ 323 public void setRawResponseInspector(final RawResponseInspector responseInspector) { 324 325 this.responseInspector = responseInspector; 326 } 327 328 329 /** 330 * Gets all non-expired HTTP cookies currently stored in the client. 331 * 332 * @return The HTTP cookies, or empty list if none were set by the 333 * server or cookies are not 334 * {@link JSONRPC2SessionOptions#acceptCookies accepted}. 335 */ 336 public List<HttpCookie> getCookies() { 337 338 if (cookieManager == null) { 339 340 return Collections.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 private void applyHeaders(final URLConnection con) { 353 354 // Expect UTF-8 for JSON 355 con.setRequestProperty("Accept-Charset", "UTF-8"); 356 357 // Add "Content-Type" header? 358 if (options.getRequestContentType() != null) 359 con.setRequestProperty("Content-Type", options.getRequestContentType()); 360 361 // Add "Origin" header? 362 if (options.getOrigin() != null) 363 con.setRequestProperty("Origin", options.getOrigin()); 364 365 // Add "Accept-Encoding: gzip, deflate" header? 366 if (options.enableCompression()) 367 con.setRequestProperty("Accept-Encoding", "gzip, deflate"); 368 369 // Add "Cookie" headers? 370 if (options.acceptCookies()) { 371 372 StringBuilder buf = new StringBuilder(); 373 374 for (HttpCookie cookie: getCookies()) { 375 376 if (buf.length() > 0) 377 buf.append("; "); 378 379 buf.append(cookie.toString()); 380 } 381 382 con.setRequestProperty("Cookie", buf.toString()); 383 } 384 } 385 386 387 /** 388 * Creates and configures a new URL connection to the JSON-RPC 2.0 389 * server endpoint according to the session settings. 390 * 391 * @return The URL connection, configured and ready for output (HTTP 392 * POST). 393 * 394 * @throws JSONRPC2SessionException If the URL connection couldn't be 395 * created or configured. 396 */ 397 private URLConnection createURLConnection() 398 throws JSONRPC2SessionException { 399 400 // Open HTTP connection 401 URLConnection con; 402 403 try { 404 // Use proxy? 405 if (options.getProxy() != null) 406 con = url.openConnection(options.getProxy()); 407 else 408 con = url.openConnection(); 409 410 } catch (IOException e) { 411 412 throw new JSONRPC2SessionException( 413 "Network exception: " + e.getMessage(), 414 JSONRPC2SessionException.NETWORK_EXCEPTION, 415 e); 416 } 417 418 con.setConnectTimeout(options.getConnectTimeout()); 419 con.setReadTimeout(options.getReadTimeout()); 420 421 applyHeaders(con); 422 423 // Set POST mode 424 con.setDoOutput(true); 425 426 // Set trust all certs SSL factory? 427 if (con instanceof HttpsURLConnection && options.trustsAllCerts()) { 428 429 if (trustAllSocketFactory == null) { 430 // TODO 431 throw new JSONRPC2SessionException("Couldn't obtain trust-all SSL socket factory"); 432 } 433 434 ((HttpsURLConnection)con).setSSLSocketFactory(trustAllSocketFactory); 435 } 436 437 // Apply connection configurator? 438 if (connectionConfigurator != null) 439 connectionConfigurator.configure((HttpURLConnection)con); 440 441 return con; 442 } 443 444 445 /** 446 * Posts string data (i.e. JSON string) to the specified URL 447 * connection. 448 * 449 * @param con The URL connection. Must be in HTTP POST mode. Must not 450 * be {@code null}. 451 * @param data The string data to post. Must not be {@code null}. 452 * 453 * @throws JSONRPC2SessionException If an I/O exception is encountered. 454 */ 455 private static void postString(final URLConnection con, final String data) 456 throws JSONRPC2SessionException { 457 458 try { 459 OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream(), "UTF-8"); 460 wr.write(data); 461 wr.flush(); 462 wr.close(); 463 464 } catch (IOException e) { 465 466 throw new JSONRPC2SessionException( 467 "Network exception: " + e.getMessage(), 468 JSONRPC2SessionException.NETWORK_EXCEPTION, 469 e); 470 } 471 } 472 473 474 /** 475 * Reads the raw response from an URL connection (after HTTP POST). 476 * Invokes the {@link RawResponseInspector} if configured and stores 477 * any cookies {@link JSONRPC2SessionOptions#acceptCookies()} if 478 * 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; 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 * Closes the specified URL connection by closing the underlying I/O 537 * streams. Java may cache the TCP socket for later reuse, see 538 * http://stackoverflow.com/a/11533423/429425 539 * 540 * @param con The URL connection to close. May be {@code null}. 541 */ 542 private static void closeURLConnection(final URLConnection con) { 543 544 if (con == null) { 545 return; 546 } 547 548 try { 549 con.getInputStream().close(); 550 con.getOutputStream().close(); 551 } catch (Exception e) { 552 // ignore 553 } 554 } 555 556 557 /** 558 * Sends a JSON-RPC 2.0 request using HTTP POST and returns the server 559 * response. 560 * 561 * @param request The JSON-RPC 2.0 request to send. Must not be 562 * {@code null}. 563 * 564 * @return The JSON-RPC 2.0 response returned by the server. 565 * 566 * @throws JSONRPC2SessionException On a network error, unexpected HTTP 567 * response content type or invalid 568 * JSON-RPC 2.0 response. 569 */ 570 public JSONRPC2Response send(final JSONRPC2Request request) 571 throws JSONRPC2SessionException { 572 573 // Create and configure URL connection to server endpoint 574 URLConnection con = createURLConnection(); 575 576 final RawResponse rawResponse; 577 578 try { 579 // Send request encoded as JSON 580 postString(con, request.toString()); 581 582 // Get the response 583 rawResponse = readRawResponse(con); 584 585 } finally { 586 closeURLConnection(con); 587 } 588 589 // Check response content type 590 String contentType = rawResponse.getContentType(); 591 592 if (! options.isAllowedResponseContentType(contentType)) { 593 594 String msg; 595 596 if (contentType == null) 597 msg = "Missing Content-Type header in the HTTP response"; 598 else 599 msg = "Unexpected \"" + contentType + "\" content type of the HTTP response"; 600 601 throw new JSONRPC2SessionException(msg, JSONRPC2SessionException.UNEXPECTED_CONTENT_TYPE); 602 } 603 604 // Parse and return the response 605 JSONRPC2Response response; 606 607 try { 608 response = JSONRPC2Response.parse(rawResponse.getContent(), 609 options.preservesParseOrder(), 610 options.ignoresVersion(), 611 options.parsesNonStdAttributes()); 612 613 } catch (JSONRPC2ParseException e) { 614 615 throw new JSONRPC2SessionException( 616 "Invalid JSON-RPC 2.0 response", 617 JSONRPC2SessionException.BAD_RESPONSE, 618 e); 619 } 620 621 // Response ID must match the request ID, except for 622 // -32700 (parse error), -32600 (invalid request) and 623 // -32603 (internal error) 624 625 Object reqID = request.getID(); 626 Object resID = response.getID(); 627 628 if (reqID != null && resID != null && reqID.toString().equals(resID.toString()) ) { 629 // ok 630 } else if (reqID == null && resID == null) { 631 // ok 632 } else if (! response.indicatesSuccess() && ( response.getError().getCode() == -32700 || 633 response.getError().getCode() == -32600 || 634 response.getError().getCode() == -32603 )) { 635 // ok 636 } else { 637 throw new JSONRPC2SessionException( 638 "Invalid JSON-RPC 2.0 response: ID mismatch: Returned " + 639 resID + ", expected " + reqID, 640 JSONRPC2SessionException.BAD_RESPONSE); 641 } 642 643 return response; 644 } 645 646 647 /** 648 * Sends a JSON-RPC 2.0 notification using HTTP POST. Note that 649 * contrary to requests, notifications produce no server response. 650 * 651 * @param notification The JSON-RPC 2.0 notification to send. Must not 652 * be {@code null}. 653 * 654 * @throws JSONRPC2SessionException On a network error. 655 */ 656 public void send(final JSONRPC2Notification notification) 657 throws JSONRPC2SessionException { 658 659 // Create and configure URL connection to server endpoint 660 URLConnection con = createURLConnection(); 661 662 // Send notification encoded as JSON 663 try { 664 postString(con, notification.toString()); 665 666 } finally { 667 668 closeURLConnection(con); 669 } 670 } 671} 672