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 */ 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 throw new JSONRPC2SessionException("Couldn't obtain trust-all SSL socket factory"); 430 431 ((HttpsURLConnection)con).setSSLSocketFactory(trustAllSocketFactory); 432 } 433 434 // Apply connection configurator? 435 if (connectionConfigurator != null) 436 connectionConfigurator.configure((HttpURLConnection)con); 437 438 return con; 439 } 440 441 442 /** 443 * Posts string data (i.e. JSON string) to the specified URL 444 * connection. 445 * 446 * @param con The URL connection. Must be in HTTP POST mode. Must not 447 * be {@code null}. 448 * @param data The string data to post. Must not be {@code null}. 449 * 450 * @throws JSONRPC2SessionException If an I/O exception is encountered. 451 */ 452 private static void postString(final URLConnection con, final String data) 453 throws JSONRPC2SessionException { 454 455 try { 456 OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream(), "UTF-8"); 457 wr.write(data); 458 wr.flush(); 459 wr.close(); 460 461 } catch (IOException e) { 462 463 throw new JSONRPC2SessionException( 464 "Network exception: " + e.getMessage(), 465 JSONRPC2SessionException.NETWORK_EXCEPTION, 466 e); 467 } 468 } 469 470 471 /** 472 * Reads the raw response from an URL connection (after HTTP POST). 473 * Invokes the {@link RawResponseInspector} if configured and stores 474 * any cookies {@link JSONRPC2SessionOptions#acceptCookies()} if 475 * required}. 476 * 477 * @param con The URL connection. It should contain ready data for 478 * retrieval. Must not be {@code null}. 479 * 480 * @return The raw response. 481 * 482 * @throws JSONRPC2SessionException If an exception is encountered. 483 */ 484 private RawResponse readRawResponse(final URLConnection con) 485 throws JSONRPC2SessionException { 486 487 RawResponse rawResponse; 488 489 try { 490 rawResponse = RawResponse.parse((HttpURLConnection)con); 491 492 } catch (IOException e) { 493 494 throw new JSONRPC2SessionException( 495 "Network exception: " + e.getMessage(), 496 JSONRPC2SessionException.NETWORK_EXCEPTION, 497 e); 498 } 499 500 if (responseInspector != null) 501 responseInspector.inspect(rawResponse); 502 503 if (options.acceptCookies()) { 504 505 // Init cookie manager? 506 if (cookieManager == null) 507 cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL); 508 509 try { 510 cookieManager.put(con.getURL().toURI(), rawResponse.getHeaderFields()); 511 512 } catch (URISyntaxException e) { 513 514 throw new JSONRPC2SessionException( 515 "Network exception: " + e.getMessage(), 516 JSONRPC2SessionException.NETWORK_EXCEPTION, 517 e); 518 519 } catch (IOException e) { 520 521 throw new JSONRPC2SessionException( 522 "I/O exception: " + e.getMessage(), 523 JSONRPC2SessionException.NETWORK_EXCEPTION, 524 e); 525 } 526 } 527 528 return rawResponse; 529 } 530 531 532 /** 533 * Sends a JSON-RPC 2.0 request using HTTP POST and returns the server 534 * response. 535 * 536 * @param request The JSON-RPC 2.0 request to send. Must not be 537 * {@code null}. 538 * 539 * @return The JSON-RPC 2.0 response returned by the server. 540 * 541 * @throws JSONRPC2SessionException On a network error, unexpected HTTP 542 * response content type or invalid 543 * JSON-RPC 2.0 response. 544 */ 545 public JSONRPC2Response send(final JSONRPC2Request request) 546 throws JSONRPC2SessionException { 547 548 // Create and configure URL connection to server endpoint 549 URLConnection con = createURLConnection(); 550 551 // Send request encoded as JSON 552 postString(con, request.toString()); 553 554 // Get the response 555 RawResponse rawResponse = readRawResponse(con); 556 557 // Check response content type 558 String contentType = rawResponse.getContentType(); 559 560 if (! options.isAllowedResponseContentType(contentType)) { 561 562 String msg; 563 564 if (contentType == null) 565 msg = "Missing Content-Type header in the HTTP response"; 566 else 567 msg = "Unexpected \"" + contentType + "\" content type of the HTTP response"; 568 569 throw new JSONRPC2SessionException(msg, JSONRPC2SessionException.UNEXPECTED_CONTENT_TYPE); 570 } 571 572 // Parse and return the response 573 JSONRPC2Response response; 574 575 try { 576 response = JSONRPC2Response.parse(rawResponse.getContent(), 577 options.preservesParseOrder(), 578 options.ignoresVersion(), 579 options.parsesNonStdAttributes()); 580 581 } catch (JSONRPC2ParseException e) { 582 583 throw new JSONRPC2SessionException( 584 "Invalid JSON-RPC 2.0 response", 585 JSONRPC2SessionException.BAD_RESPONSE, 586 e); 587 } 588 589 // Response ID must match the request ID, except for 590 // -32700 (parse error), -32600 (invalid request) and 591 // -32603 (internal error) 592 593 Object reqID = request.getID(); 594 Object resID = response.getID(); 595 596 if (reqID != null && resID != null && reqID.toString().equals(resID.toString()) ) { 597 // ok 598 } else if (reqID == null && resID == null) { 599 // ok 600 } else if (! response.indicatesSuccess() && ( response.getError().getCode() == -32700 || 601 response.getError().getCode() == -32600 || 602 response.getError().getCode() == -32603 )) { 603 // ok 604 } else { 605 throw new JSONRPC2SessionException( 606 "Invalid JSON-RPC 2.0 response: ID mismatch: Returned " + 607 resID + ", expected " + reqID, 608 JSONRPC2SessionException.BAD_RESPONSE); 609 } 610 611 return response; 612 } 613 614 615 /** 616 * Sends a JSON-RPC 2.0 notification using HTTP POST. Note that 617 * contrary to requests, notifications produce no server response. 618 * 619 * @param notification The JSON-RPC 2.0 notification to send. Must not 620 * be {@code null}. 621 * 622 * @throws JSONRPC2SessionException On a network error. 623 */ 624 public void send(final JSONRPC2Notification notification) 625 throws JSONRPC2SessionException { 626 627 // Create and configure URL connection to server endpoint 628 URLConnection con = createURLConnection(); 629 630 // Send notification encoded as JSON 631 postString(con, notification.toString()); 632 } 633} 634