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