1 /*
2  * Copyright 2006 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.google.gwt.user.server.rpc;
17 
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.text.ParseException;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.zip.GZIPOutputStream;
27 
28 import javax.servlet.ServletContext;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.HttpServlet;
31 import javax.servlet.http.HttpServletRequest;
32 import javax.servlet.http.HttpServletResponse;
33 
34 import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
35 import com.google.gwt.user.client.rpc.SerializationException;
36 
37 /**
38  * RemoteServiceServlet changes to allow extensions required for Jetty Continuatution support.
39  *
40  * Changes:
41  *
42  * readPayloadAsUtf8 now protected non-static
43  *
44  * @author Craig Day (craig@alderaan.com.au)
45  *
46  */
47 public class OpenRemoteServiceServlet extends HttpServlet implements SerializationPolicyProvider {
48     /*
49      * These members are used to get and set the different HttpServletResponse and
50      * HttpServletRequest headers.
51      */
52     private static final String ACCEPT_ENCODING = "Accept-Encoding";
53     private static final String CHARSET_UTF8 = "UTF-8";
54     private static final String CONTENT_ENCODING = "Content-Encoding";
55     private static final String CONTENT_ENCODING_GZIP = "gzip";
56     private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8";
57     private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";
58     private static final String EXPECTED_CONTENT_TYPE = "text/x-gwt-rpc";
59     private static final String EXPECTED_CHARSET = "charset=utf-8";
60 
61     /**
62      * Controls the compression threshold at and below which no compression will
63      * take place.
64      */
65     private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256;
66 
67     /**
68      * Return true if the response object accepts Gzip encoding. This is done by
69      * checking that the accept-encoding header specifies gzip as a supported
70      * encoding.
71      */
acceptsGzipEncoding(HttpServletRequest request)72     private static boolean acceptsGzipEncoding(HttpServletRequest request) {
73         assert (request != null);
74 
75         String acceptEncoding = request.getHeader(ACCEPT_ENCODING);
76         if (null == acceptEncoding) {
77             return false;
78         }
79 
80         return (acceptEncoding.indexOf(CONTENT_ENCODING_GZIP) != -1);
81     }
82 
83     /**
84      * This method attempts to estimate the number of bytes that a string will
85      * consume when it is sent out as part of an HttpServletResponse. This really
86      * a hack since we are assuming that every character will consume two bytes
87      * upon transmission. This is definitely not true since some characters
88      * actually consume more than two bytes and some consume less. This is even
89      * less accurate if the string is converted to UTF8. However, it does save us
90      * from converting every string that we plan on sending back to UTF8 just to
91      * determine that we should not compress it.
92      */
estimateByteSize(final String buffer)93     private static int estimateByteSize(final String buffer) {
94         return (buffer.length() * 2);
95     }
96 
97     /**
98      * Read the payload as UTF-8 from the request stream.
99      */
readPayloadAsUtf8(HttpServletRequest request)100     protected String readPayloadAsUtf8(HttpServletRequest request)
101             throws IOException, ServletException {
102         int contentLength = request.getContentLength();
103         if (contentLength == -1) {
104             // Content length must be known.
105             throw new ServletException("Content-Length must be specified");
106         }
107 
108         String contentType = request.getContentType();
109         boolean contentTypeIsOkay = false;
110         // Content-Type must be specified.
111         /*if (contentType != null) {
112             // The type must be plain text.
113             if (contentType.startsWith("text/plain")) {
114                 // And it must be UTF-8 encoded (or unspecified, in which case we assume
115                 // that it's either UTF-8 or ASCII).
116                 if (contentType.indexOf("charset=") == -1) {
117                     contentTypeIsOkay = true;
118                 } else if (contentType.indexOf("charset=utf-8") != -1) {
119                     contentTypeIsOkay = true;
120                 }
121             }
122         }
123         if (!contentTypeIsOkay) {
124             throw new ServletException(
125                     "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)");
126         }*/
127         if (contentType != null) {
128             contentType = contentType.toLowerCase();
129             /*
130              * The Content-Type must be text/x-gwt-rpc.
131              *
132              * NOTE:We use startsWith because some servlet engines, i.e. Tomcat, do
133              * not remove the charset component but others do.
134              */
135             if (contentType.startsWith(EXPECTED_CONTENT_TYPE)) {
136                 String characterEncoding = request.getCharacterEncoding();
137                 if (characterEncoding != null) {
138                     /*
139                      * TODO: It would seem that we should be able to use equalsIgnoreCase
140                      * here instead of indexOf. Need to be sure that servlet engines
141                      * return a properly parsed character encoding string if we decide to
142                      * make this change.
143                      */
144                     if (characterEncoding.toLowerCase().indexOf(CHARSET_UTF8.toLowerCase()) != -1)
145                         contentTypeIsOkay = true;
146                 }
147             }
148         }
149         if (!contentTypeIsOkay) {
150             throw new ServletException("Content-Type must be '"
151                     + EXPECTED_CONTENT_TYPE + "' with '" + EXPECTED_CHARSET + "'.");
152         }
153         InputStream in = request.getInputStream();
154         try {
155             byte[] payload = new byte[contentLength];
156             int offset = 0;
157             int len = contentLength;
158             int byteCount;
159             while (offset < contentLength) {
160                 byteCount = in.read(payload, offset, len);
161                 if (byteCount == -1) {
162                     throw new ServletException("Client did not send " + contentLength
163                             + " bytes as expected");
164                 }
165                 offset += byteCount;
166                 len -= byteCount;
167             }
168             return new String(payload, "UTF-8");
169         } finally {
170             if (in != null) {
171                 in.close();
172             }
173         }
174     }
175 
176     private final ThreadLocal perThreadRequest = new ThreadLocal();
177 
178     private final ThreadLocal perThreadResponse = new ThreadLocal();
179 
180     /**
181      * A cache of moduleBaseURL and serialization policy strong name to
182      * {@link SerializationPolicy}.
183      */
184     private final Map<String, SerializationPolicy> serializationPolicyCache = new HashMap<String, SerializationPolicy>();
185 
186     /**
187      * The default constructor.
188      */
OpenRemoteServiceServlet()189     public OpenRemoteServiceServlet() {
190     }
191 
192     /**
193      * Standard HttpServlet method: handle the POST.
194      * <p/>
195      * This doPost method swallows ALL exceptions, logs them in the
196      * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code
197      * 500.
198      */
doPost(HttpServletRequest request, HttpServletResponse response)199     public final void doPost(HttpServletRequest request,
200                              HttpServletResponse response) {
201         try {
202             // Store the request & response objects in thread-local storage.
203             //
204             perThreadRequest.set(request);
205             perThreadResponse.set(response);
206 
207             // Read the request fully.
208             //
209             String requestPayload = readPayloadAsUtf8(request);
210 
211             // Let subclasses see the serialized request.
212             //
213             onBeforeRequestDeserialized(requestPayload);
214 
215             // Invoke the core dispatching logic, which returns the serialized
216             // result.
217             //
218             String responsePayload = processCall(requestPayload);
219 
220             // Let subclasses see the serialized response.
221             //
222             onAfterResponseSerialized(responsePayload);
223 
224             // Write the response.
225             //
226             writeResponse(request, response, responsePayload);
227             return;
228         } catch (Throwable e) {
229             // Give a subclass a chance to either handle the exception or rethrow it
230             //
231             doUnexpectedFailure(e);
232         } finally {
233             // null the thread-locals to avoid holding request/response
234             //
235             perThreadRequest.set(null);
236             perThreadResponse.set(null);
237         }
238     }
239 
240     /**
241      * Process a call originating from the given request. Uses the
242      * {@link RPC#invokeAndEncodeResponse(Object,java.lang.reflect.Method,Object[])}
243      * method to do the actual work.
244      * <p/>
245      * Subclasses may optionally override this method to handle the payload in any
246      * way they desire (by routing the request to a framework component, for
247      * instance). The {@link HttpServletRequest} and {@link HttpServletResponse}
248      * can be accessed via the {@link #getThreadLocalRequest()} and
249      * {@link #getThreadLocalResponse()} methods.
250      * </p>
251      * This is public so that it can be unit tested easily without HTTP.
252      *
253      * @param payload the UTF-8 request payload
254      * @return a string which encodes either the method's return, a checked
255      *         exception thrown by the method, or an
256      *         {@link IncompatibleRemoteServiceException}
257      * @throws SerializationException if we cannot serialize the response
258      * @throws UnexpectedException    if the invocation throws a checked exception
259      *                                that is not declared in the service method's signature
260      * @throws RuntimeException       if the service method throws an unchecked
261      *                                exception (the exception will be the one thrown by the service)
262      */
processCall(String payload)263     public String processCall(String payload) throws SerializationException {
264         try {
265             RPCRequest rpcRequest = RPC.decodeRequest(payload, this.getClass(), this);
266             return RPC.invokeAndEncodeResponse(this, rpcRequest.getMethod(),
267                     rpcRequest.getParameters(), rpcRequest.getSerializationPolicy());
268         } catch (IncompatibleRemoteServiceException ex) {
269             return RPC.encodeResponseForFailure(null, ex);
270         }
271     }
272 
273     /**
274      * Override this method to control what should happen when an exception
275      * escapes the {@link #processCall(String)} method. The default implementation
276      * will log the failure and send a generic failure response to the client.<p/>
277      * <p/>
278      * An "expected failure" is an exception thrown by a service method that is
279      * declared in the signature of the service method. These exceptions are
280      * serialized back to the client, and are not passed to this method. This
281      * method is called only for exceptions or errors that are not part of the
282      * service method's signature, or that result from SecurityExceptions,
283      * SerializationExceptions, or other failures within the RPC framework.<p/>
284      * <p/>
285      * Note that if the desired behavior is to both send the GENERIC_FAILURE_MSG
286      * response AND to rethrow the exception, then this method should first send
287      * the GENERIC_FAILURE_MSG response itself (using getThreadLocalResponse), and
288      * then rethrow the exception. Rethrowing the exception will cause it to
289      * escape into the servlet container.
290      *
291      * @param e the exception which was thrown
292      */
doUnexpectedFailure(Throwable e)293     protected void doUnexpectedFailure(Throwable e) {
294         ServletContext servletContext = getServletContext();
295         servletContext.log("Exception while dispatching incoming RPC call", e);
296 
297         // Send GENERIC_FAILURE_MSG with 500 status.
298         //
299         respondWithFailure(getThreadLocalResponse());
300     }
301 
302     /**
303      * Gets the <code>HttpServletRequest</code> object for the current call. It
304      * is stored thread-locally so that simultaneous invocations can have
305      * different request objects.
306      */
getThreadLocalRequest()307     protected final HttpServletRequest getThreadLocalRequest() {
308         return (HttpServletRequest) perThreadRequest.get();
309     }
310 
311     /**
312      * Gets the <code>HttpServletResponse</code> object for the current call. It
313      * is stored thread-locally so that simultaneous invocations can have
314      * different response objects.
315      */
getThreadLocalResponse()316     protected final HttpServletResponse getThreadLocalResponse() {
317         return (HttpServletResponse) perThreadResponse.get();
318     }
319 
320     /**
321      * Override this method to examine the serialized response that will be
322      * returned to the client. The default implementation does nothing and need
323      * not be called by subclasses.
324      */
onAfterResponseSerialized(String serializedResponse)325     protected void onAfterResponseSerialized(String serializedResponse) {
326     }
327 
328     /**
329      * Override this method to examine the serialized version of the request
330      * payload before it is deserialized into objects. The default implementation
331      * does nothing and need not be called by subclasses.
332      */
onBeforeRequestDeserialized(String serializedRequest)333     protected void onBeforeRequestDeserialized(String serializedRequest) {
334     }
335 
336     /**
337      * Determines whether the response to a given servlet request should or should
338      * not be GZIP compressed. This method is only called in cases where the
339      * requestor accepts GZIP encoding.
340      * <p/>
341      * This implementation currently returns <code>true</code> if the response
342      * string's estimated byte length is longer than 256 bytes. Subclasses can
343      * override this logic.
344      * </p>
345      *
346      * @param request         the request being served
347      * @param response        the response that will be written into
348      * @param responsePayload the payload that is about to be sent to the client
349      * @return <code>true</code> if responsePayload should be GZIP compressed,
350      *         otherwise <code>false</code>.
351      */
shouldCompressResponse(HttpServletRequest request, HttpServletResponse response, String responsePayload)352     protected boolean shouldCompressResponse(HttpServletRequest request,
353                                              HttpServletResponse response, String responsePayload) {
354         return estimateByteSize(responsePayload) > UNCOMPRESSED_BYTE_SIZE_LIMIT;
355     }
356 
357     /**
358      * Called when the machinery of this class itself has a problem, rather than
359      * the invoked third-party method. It writes a simple 500 message back to the
360      * client.
361      */
respondWithFailure(HttpServletResponse response)362     private void respondWithFailure(HttpServletResponse response) {
363         try {
364             response.setContentType("text/plain");
365             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
366             response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes());
367         } catch (IOException e) {
368             getServletContext().log(
369                     "respondWithFailure failed while sending the previous failure to the client",
370                     e);
371         }
372     }
373 
374     /**
375      * Write the response payload to the response stream.
376      */
writeResponse(HttpServletRequest request, HttpServletResponse response, String responsePayload)377     private void writeResponse(HttpServletRequest request,
378                                HttpServletResponse response, String responsePayload) throws IOException {
379 
380         byte[] reply = responsePayload.getBytes(CHARSET_UTF8);
381         String contentType = CONTENT_TYPE_TEXT_PLAIN_UTF8;
382 
383         if (acceptsGzipEncoding(request)
384                 && shouldCompressResponse(request, response, responsePayload)) {
385             // Compress the reply and adjust headers.
386             //
387             ByteArrayOutputStream output = null;
388             GZIPOutputStream gzipOutputStream = null;
389             Throwable caught = null;
390             try {
391                 output = new ByteArrayOutputStream(reply.length);
392                 gzipOutputStream = new GZIPOutputStream(output);
393                 gzipOutputStream.write(reply);
394                 gzipOutputStream.finish();
395                 gzipOutputStream.flush();
396                 response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP);
397                 reply = output.toByteArray();
398             } catch (IOException e) {
399                 caught = e;
400             } finally {
401                 if (null != gzipOutputStream) {
402                     gzipOutputStream.close();
403                 }
404                 if (null != output) {
405                     output.close();
406                 }
407             }
408 
409             if (caught != null) {
410                 getServletContext().log("Unable to compress response", caught);
411                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
412                 return;
413             }
414         }
415 
416         // Send the reply.
417         //
418         response.setContentLength(reply.length);
419         response.setContentType(contentType);
420         response.setStatus(HttpServletResponse.SC_OK);
421         response.getOutputStream().write(reply);
422     }
423 
getSerializationPolicy(String moduleBaseURL, String strongName)424     public final SerializationPolicy getSerializationPolicy(String moduleBaseURL, String strongName)
425     {
426         SerializationPolicy serializationPolicy = getCachedSerializationPolicy(moduleBaseURL,
427                 strongName);
428         if (serializationPolicy != null)
429             return serializationPolicy;
430 
431 
432         serializationPolicy = doGetSerializationPolicy(getThreadLocalRequest(),
433                 moduleBaseURL, strongName);
434 
435         if (serializationPolicy == null)
436         {
437             // Failed to get the requested serialization policy; use the default
438             getServletContext().log(
439                 "WARNING: Failed to get the SerializationPolicy '"
440                     + strongName
441                     + "' for module '"
442                     + moduleBaseURL
443                     + "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
444             serializationPolicy = RPC.getDefaultSerializationPolicy();
445         }
446 
447         // This could cache null or an actual instance. Either way we will not
448         // attempt to lookup the policy again.
449         putCachedSerializationPolicy(moduleBaseURL, strongName, serializationPolicy);
450 
451         return serializationPolicy;
452     }
453 
getCachedSerializationPolicy(String moduleBaseURL, String strongName)454     private SerializationPolicy getCachedSerializationPolicy(String moduleBaseURL,
455             String strongName)
456     {
457         synchronized (serializationPolicyCache)
458         {
459             return serializationPolicyCache.get(moduleBaseURL + strongName);
460         }
461     }
462 
putCachedSerializationPolicy(String moduleBaseURL, String strongName, SerializationPolicy serializationPolicy)463     private void putCachedSerializationPolicy(String moduleBaseURL, String strongName,
464             SerializationPolicy serializationPolicy)
465     {
466         synchronized (serializationPolicyCache)
467         {
468             serializationPolicyCache.put(moduleBaseURL + strongName, serializationPolicy);
469         }
470     }
471 
472     /**
473      * Gets the {@link SerializationPolicy} for given module base URL and strong
474      * name if there is one.
475      *
476      * Override this method to provide a {@link SerializationPolicy} using an
477      * alternative approach.
478      *
479      * @param request the HTTP request being serviced
480      * @param moduleBaseURL as specified in the incoming payload
481      * @param strongName a strong name that uniquely identifies a serialization
482      *          policy file
483      * @return a {@link SerializationPolicy} for the given module base URL and
484      *         strong name, or <code>null</code> if there is none
485      */
doGetSerializationPolicy(HttpServletRequest request, String moduleBaseURL, String strongName)486     protected SerializationPolicy doGetSerializationPolicy(HttpServletRequest request,
487             String moduleBaseURL, String strongName)
488     {
489         // The request can tell you the path of the web app relative to the
490         // container root.
491         String contextPath = request.getContextPath();
492 
493         String modulePath = null;
494         if (moduleBaseURL != null)
495         {
496             try
497             {
498                 modulePath = new URL(moduleBaseURL).getPath();
499             }
500             catch (MalformedURLException ex)
501             {
502                 // log the information, we will default
503                 getServletContext().log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
504             }
505         }
506 
507         SerializationPolicy serializationPolicy = null;
508 
509         /*
510          * Check that the module path must be in the same web app as the servlet
511          * itself. If you need to implement a scheme different than this, override
512          * this method.
513          */
514         if (modulePath == null || !modulePath.startsWith(contextPath))
515         {
516             String message = "ERROR: The module path requested, "
517                 + modulePath
518                 + ", is not in the same web application as this servlet, "
519                 + contextPath
520                 + ".  Your module may not be properly configured or your client and server code maybe out of date.";
521             getServletContext().log(message);
522         }
523         else
524         {
525             // Strip off the context path from the module base URL. It should be a
526             // strict prefix.
527             String contextRelativePath = modulePath.substring(contextPath.length());
528 
529             String serializationPolicyFilePath = SerializationPolicyLoader.getSerializationPolicyFileName(contextRelativePath
530                 + strongName);
531 
532             // Open the RPC resource file read its contents.
533             InputStream is = getServletContext().getResourceAsStream(
534                 serializationPolicyFilePath);
535             try
536             {
537                 if (is != null)
538                 {
539                     try
540                     {
541                         serializationPolicy = SerializationPolicyLoader.loadFromStream(is, null);
542                     }
543                     catch (ParseException e)
544                     {
545                         getServletContext().log(
546                                 "ERROR: Failed to parse the policy file '"
547                                     + serializationPolicyFilePath + "'", e);
548                     }
549                     catch (IOException e)
550                     {
551                         getServletContext().log(
552                                 "ERROR: Could not read the policy file '"
553                                     + serializationPolicyFilePath + "'", e);
554                     }
555                 }
556                 else
557                 {
558                     String message = "ERROR: The serialization policy file '"
559                         + serializationPolicyFilePath
560                         + "' was not found; did you forget to include it in this deployment?";
561                     getServletContext().log(message);
562                 }
563             }
564             finally
565             {
566                 if (is != null)
567                 {
568                     try
569                     {
570                         is.close();
571                     }
572                     catch (IOException e)
573                     {
574                         // Ignore this error
575                     }
576                 }
577             }
578         }
579 
580         return serializationPolicy;
581     }
582 
583 
584 }
585