Author: johnh
Date: Mon Aug  2 23:12:41 2010
New Revision: 981703

URL: http://svn.apache.org/viewvc?rev=981703&view=rev
Log:
This patch involves the following changes:

- ProxyHandler and AccelHandler use similar code for handling the headers,
status codes and rewrite-mime-types. This patch refactors ProxyHandler to use
the common code placed in UriUtils for all of the above. The
disallowed-headers-list has also been consolidated into UriUtils.

- As a side effect of the refactoring, the header names and values will be
validated (via the UriUtils method) for the ProxyHandler flow as well.

- A bug in the handling of the "rewrite-mime-type" logic has also been fixed
here. Earlier the response mime type was only checked for its prefix match with
the rewrite mime type upto the "/" marker and not including the "/" marker -
this has been fixed now.

- Tests have been added for the "rewrite-mime-type" handling.

Patch provided by Anupama Dutta.


Modified:
    shindig/trunk/java/common/conf/shindig.properties
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java

Modified: shindig/trunk/java/common/conf/shindig.properties
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/common/conf/shindig.properties?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- shindig/trunk/java/common/conf/shindig.properties (original)
+++ shindig/trunk/java/common/conf/shindig.properties Mon Aug  2 23:12:41 2010
@@ -146,3 +146,4 @@ shindig.json-rpc.result-field=result
 # "Bad Gateway error"s, so that it is clear to the user that the proxy server 
is
 # the one that threw the exception.
 shindig.accelerate.remapInternalServerError=true
+shindig.proxy.remapInternalServerError=true

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java
 Mon Aug  2 23:12:41 2010
@@ -109,7 +109,7 @@ public class AccelHandler extends ProxyB
 
     // Override the content type of the final http response if the input 
request
     // had the rewrite mime type header.
-    rewriteContentType(req, response);
+    UriUtils.maybeRewriteContentType(req, response);
 
     // Copy the content.
     IOUtils.copy(results.getResponse(), response.getOutputStream());
@@ -195,23 +195,6 @@ public class AccelHandler extends ProxyB
   }
 
   /**
-   * Rewrite the content type of the final http response if the request has the
-   * rewrite-mime-type param.
-   * @param req The http request.
-   * @param response The final http response to be returned to user.
-   */
-  protected void rewriteContentType(HttpRequest req, HttpServletResponse 
response) {
-    String contentType = response.getContentType();
-    String requiredType = req.getRewriteMimeType();
-    if (!StringUtils.isEmpty(requiredType)) {
-      if (requiredType.endsWith("/*") && !StringUtils.isEmpty(contentType)) {
-        requiredType = requiredType.substring(0, requiredType.length() - 2);
-      }
-      response.setContentType(requiredType);
-    }
-  }
-
-  /**
    * Process errors when fetching uri using request pipeline and return the
    * error response to be returned to the user if any.
    * @param results The http response returned by request pipeline.

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
 Mon Aug  2 23:12:41 2010
@@ -55,11 +55,6 @@ public abstract class ProxyBase {
   public static final String REWRITE_MIME_TYPE_PARAM = 
Param.REWRITE_MIME_TYPE.getKey();
   public static final String SANITIZE_CONTENT_PARAM = Param.SANITIZE.getKey();
 
-  protected static final Set<String> DISALLOWED_RESPONSE_HEADERS = 
ImmutableSet.of(
-      "set-cookie", "content-length", "content-encoding", "etag", 
"last-modified" ,"accept-ranges",
-      "vary", "expires", "date", "pragma", "cache-control", 
"transfer-encoding", "www-authenticate"
-  );
-
   private static final Logger LOG = 
Logger.getLogger(ProxyBase.class.getName());
   
   /**

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
 Mon Aug  2 23:12:41 2010
@@ -20,6 +20,7 @@ package org.apache.shindig.gadgets.servl
 
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
@@ -34,6 +35,7 @@ import org.apache.shindig.gadgets.http.R
 import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
 import org.apache.shindig.gadgets.rewrite.RewritingException;
 import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.UriUtils;
 
 import java.io.IOException;
 import java.util.Map;
@@ -57,16 +59,20 @@ public class ProxyHandler extends ProxyB
   private final LockedDomainService lockedDomainService;
   private final ResponseRewriterRegistry contentRewriterRegistry;
   private final ProxyUriManager proxyUriManager;
+  protected final boolean remapInternalServerError;
 
   @Inject
   public ProxyHandler(RequestPipeline requestPipeline,
                       LockedDomainService lockedDomainService,
                       ResponseRewriterRegistry contentRewriterRegistry,
-                      ProxyUriManager proxyUriManager) {
+                      ProxyUriManager proxyUriManager,
+                      @Named("shindig.proxy.remapInternalServerError")
+                      Boolean remapInternalServerError) {
     this.requestPipeline = requestPipeline;
     this.lockedDomainService = lockedDomainService;
     this.contentRewriterRegistry = contentRewriterRegistry;
     this.proxyUriManager = proxyUriManager;
+    this.remapInternalServerError = remapInternalServerError;
   }
 
   /**
@@ -137,46 +143,20 @@ public class ProxyHandler extends ProxyB
       }
     }
 
-    for (Map.Entry<String, String> entry : results.getHeaders().entries()) {
-      String name = entry.getKey();
-      if (!DISALLOWED_RESPONSE_HEADERS.contains(name.toLowerCase())) {
-        try {
-          response.addHeader(name, entry.getValue());
-        } catch (IllegalArgumentException e) {
-          // Skip illegal header
-          LOG.info("Skipping illegal header:  " + name + ":" + 
entry.getValue());
-        }
-      }
-    }
-
-    String responseType = results.getHeader("Content-Type");
-    if (!StringUtils.isEmpty(rcr.getRewriteMimeType())) {
-      String requiredType = rcr.getRewriteMimeType();
-      // Use a 'Vary' style check on the response
-      if (requiredType.endsWith("/*") &&
-          !StringUtils.isEmpty(responseType)) {
-        requiredType = requiredType.substring(0, requiredType.length() - 2);
-        if 
(!responseType.toLowerCase().startsWith(requiredType.toLowerCase())) {
-          response.setContentType(requiredType);
-          responseType = requiredType;
-        }
-      } else {
-        response.setContentType(requiredType);
-        responseType = requiredType;
-      }
-    }
+    // Copy the response headers and status code to the final http servlet
+    // response.
+    UriUtils.copyResponseHeadersAndStatusCode(
+        results, response, remapInternalServerError, false,
+        UriUtils.DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES,
+        UriUtils.DisallowedHeaders.CACHING_DIRECTIVES,
+        UriUtils.DisallowedHeaders.CLIENT_STATE_DIRECTIVES);
+
+    // Override the content type of the final http response if the input 
request
+    // had the rewrite mime type header.
+    UriUtils.maybeRewriteContentType(rcr, response);
 
     setResponseContentHeaders(response, results);
 
-    if (results.isError()) {
-      if (results.getHttpStatusCode() == 
HttpResponse.SC_INTERNAL_SERVER_ERROR) {
-        // External "internal error" should be mapped to gateway error
-        response.sendError(HttpResponse.SC_BAD_GATEWAY);
-      } else {
-        response.sendError(results.getHttpStatusCode());
-      }
-    } else {
-      IOUtils.copy(results.getResponse(), response.getOutputStream());
-    }
+    IOUtils.copy(results.getResponse(), response.getOutputStream());
   }
 }

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java
 Mon Aug  2 23:12:41 2010
@@ -22,10 +22,12 @@ import com.google.common.collect.Immutab
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.commons.lang.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.util.logging.Logger;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Map;
@@ -35,6 +37,7 @@ import java.util.Set;
  * Utility functions related to URI and Http servlet response management.
  */
 public class UriUtils {
+  private static final Logger LOG = Logger.getLogger(UriUtils.class.getName());
   /**
    * Enum of disallowed response headers that should not be passed on as is to
    * the user. The webserver serving out the response should be responsible
@@ -47,7 +50,7 @@ public class UriUtils {
         "accept-ranges")),
 
     CACHING_DIRECTIVES(ImmutableSet.of("vary", "expires", "date", "pragma",
-                                       "cache-control")),
+                                       "cache-control", "etag", 
"last-modified")),
 
     CLIENT_STATE_DIRECTIVES(ImmutableSet.of("set-cookie", "www-authenticate")),
 
@@ -73,7 +76,9 @@ public class UriUtils {
 
   /**
    * Returns true if the header name is valid.
-   * NOTE: RFC 822 section 3.1.2 describes the structure of header fields. 
+   * NOTE: RFC 822 section 3.1.2 describes the structure of header fields.
+   * According to the RFC, a header name (or field-name) must be composed of 
printable ASCII
+   * characters (i.e., characters that have values between 33. and 126. 
decimal, except colon).
    * @param name The header name.
    * @return True if the header name is valid, false otherwise.
    */
@@ -95,11 +100,24 @@ public class UriUtils {
   /**
    * Returns true if the header value is valid.
    * NOTE: RFC 822 section 3.1.2 describes the structure of header fields.
+   * According to the RFC, a header value (or field-body) may be composed of 
any ASCII characters,
+   * except CR or LF.
    * @param val The header value.
    * @return True if the header value is valid, false otherwise.
    */
   public static boolean isValidHeaderValue(String val) {
-    // TODO: complete this.
+    char[] dst = new char[val.length()];
+    val.getChars(0, val.length(), dst, 0);
+
+    for (char c : dst) {
+      if (c == 13 || c == 10) {
+        // CR and LF.
+        return false;
+      }
+      if (c > 127) {
+        return false;
+      }
+    }
     return true;
   }
 
@@ -133,10 +151,15 @@ public class UriUtils {
     for (Map.Entry<String, String> entry : data.getHeaders().entries()) {
       if (isValidHeaderName(entry.getKey()) && 
isValidHeaderValue(entry.getValue()) &&
           !allDisallowedHeaders.contains(entry.getKey().toLowerCase())) {
-        if (setHeaders) {
-          resp.setHeader(entry.getKey(), entry.getValue());
-        } else {
-          resp.addHeader(entry.getKey(), entry.getValue());
+        try {
+          if (setHeaders) {
+            resp.setHeader(entry.getKey(), entry.getValue());
+          } else {
+            resp.addHeader(entry.getKey(), entry.getValue());
+          }
+        } catch (IllegalArgumentException e) {
+          // Skip illegal header
+          LOG.warning("Skipping illegal header:  " + entry.getKey() + ":" + 
entry.getValue());
         }
       }
     }
@@ -210,4 +233,28 @@ public class UriUtils {
       throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
     }
   }
+
+  /**
+   * Rewrite the content type of the final http response if the request has the
+   * rewrite-mime-type param.
+   * @param req The http request.
+   * @param response The final http response to be returned to user.
+   */
+  public static void maybeRewriteContentType(HttpRequest req, 
HttpServletResponse response) {
+    String responseType = response.getContentType();
+    String requiredType = req.getRewriteMimeType();
+    if (!StringUtils.isEmpty(requiredType)) {
+      // Use a 'Vary' style check on the response
+      if (requiredType.endsWith("/*") && !StringUtils.isEmpty(responseType)) {
+        String requiredTypePrefix = requiredType.substring(0, 
requiredType.length() - 1);
+        if 
(!responseType.toLowerCase().startsWith(requiredTypePrefix.toLowerCase())) {
+          // TODO: We are currently setting the content type to something like 
x/* (e.g. text/*)
+          // which is not a valid content type. Need to fix this.
+          response.setContentType(requiredType);
+        }
+      } else {
+        response.setContentType(requiredType);
+      }
+    }
+  }
 }

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
 Mon Aug  2 23:12:41 2010
@@ -50,7 +50,7 @@ public class ProxyHandlerTest extends Se
 
   private final ProxyUriManager passthruManager = new PassthruManager();
   private final ProxyHandler proxyHandler
-      = new ProxyHandler(pipeline, lockedDomainService, rewriterRegistry, 
passthruManager);
+      = new ProxyHandler(pipeline, lockedDomainService, rewriterRegistry, 
passthruManager, true);
 
   private void expectGetAndReturnData(String url, byte[] data) throws 
Exception {
     HttpRequest req = new HttpRequest(Uri.parse(url));
@@ -243,6 +243,35 @@ public class ProxyHandlerTest extends Se
     verify();
   }
 
+  @Test
+  public void testInvalidHeaderDropped() throws Exception {
+    String url = "http://example.org/mypage.html";;
+    String domain = "example.org";
+
+    
expect(lockedDomainService.isSafeForOpenProxy(domain)).andReturn(true).atLeastOnce();
+    setupProxyRequestMock(domain, url);
+
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    String contentType = "text/html; charset=UTF-8";
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponseString("Hello")
+        .addHeader("Content-Type", contentType)
+        .addHeader("Content-Length", "200")  // Disallowed header.
+        .addHeader(":", "someDummyValue") // Invalid header name.
+        .create();
+
+    expect(pipeline.execute(req)).andReturn(resp);
+
+    replay();
+
+    proxyHandler.fetch(request, recorder);
+
+    verify();
+    assertNull(recorder.getHeader(":"));
+    assertNull(recorder.getHeader("Content-Length"));
+    assertEquals(recorder.getHeader("Content-Type"), contentType);
+  }
+  
   /**
    * Override HttpRequest equals to check for cache control fields
    */

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
 Mon Aug  2 23:12:41 2010
@@ -47,7 +47,7 @@ public class ProxyServletTest extends Se
 
   private final ProxyUriManager passthruManager = new PassthruManager();
   private final ProxyHandler proxyHandler
-      = new ProxyHandler(pipeline, lockedDomainService, null, passthruManager);
+      = new ProxyHandler(pipeline, lockedDomainService, null, passthruManager, 
true);
   private final ProxyServlet servlet = new ProxyServlet();
   private final HttpRequest internalRequest = new HttpRequest(REQUEST_URL);
   private final HttpResponse internalResponse = new 
HttpResponse(RESPONSE_BODY);

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java?rev=981703&r1=981702&r2=981703&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java
 Mon Aug  2 23:12:41 2010
@@ -25,6 +25,7 @@ import org.apache.shindig.common.uri.Uri
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.servlet.ServletTestFixture;
 import org.easymock.EasyMock;
 import org.easymock.IAnswer;
 import org.junit.Before;
@@ -40,7 +41,7 @@ import static org.easymock.EasyMock.expe
 /**
  * Tests for UriUtils.
  */
-public class UriUtilsTest extends EasyMockTestCase {
+public class UriUtilsTest extends ServletTestFixture {
   @Before
   public void setUp() throws Exception {
   }
@@ -53,6 +54,67 @@ public class UriUtilsTest extends EasyMo
     return vector.elements();
   }
 
+  private void verifyMime(String requestMime, String responseMime, String 
expectedMime)
+      throws Exception {
+    String url = "http://example.org/foo";;
+    HttpRequest req = new HttpRequest(Uri.parse(url))
+        .setRewriteMimeType(requestMime);
+    recorder.setContentType(responseMime);
+
+    UriUtils.maybeRewriteContentType(req, recorder);
+    assertEquals(expectedMime, recorder.getContentType());
+  }
+
+  @Test
+  public void testMimeMatchPass() throws Exception {
+    verifyMime("text/css", "text/css", "text/css");
+  }
+
+  @Test
+  public void testMimeMatchPassWithAdditionalAttributes() throws Exception {
+    verifyMime("text/css", "text/css; charset=UTF-8", "text/css");
+  }
+
+  @Test
+  public void testNonMatchingMime() throws Exception {
+    verifyMime("text/css", "image/png; charset=UTF-8", "text/css");
+  }
+
+  @Test
+  public void testNonMatchingMimeWithSamePrefix() throws Exception {
+    verifyMime("text/html", "text/plain", "text/html");
+  }
+
+  @Test
+  public void testNonMatchingMimeWithWildCard() throws Exception {
+    verifyMime("text/*", "image/png", "text/*");
+  }
+
+  @Test
+  public void testNonMatchingMimeWithDifferentPrefix() throws Exception {
+    verifyMime("text/*", "text123/html", "text/*");
+  }
+
+  @Test
+  public void testMimeMatchVarySupport() throws Exception {
+    verifyMime("image/*", "image/gif", "image/gif");
+  }
+
+  @Test
+  public void testNullRequestMime() throws Exception {
+    verifyMime(null, "image/png; charset=UTF-8", "image/png; charset=UTF-8");
+  }
+
+  @Test
+  public void testEmptyRequestMime() throws Exception {
+    verifyMime("", "image/png; charset=UTF-8", "image/png; charset=UTF-8");
+  }
+
+  @Test
+  public void testNullResponseMime() throws Exception {
+    verifyMime("text/*", null, "text/*");
+  }
+
   @Test
   public void testCopyResponseHeadersAndStatusCode_AddHeader() throws 
Exception {
     HttpResponse resp = new HttpResponseBuilder()


Reply via email to