You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by et...@apache.org on 2008/06/11 11:33:23 UTC

svn commit: r666593 [1/2] - in /incubator/shindig/trunk/java/gadgets/src: main/java/org/apache/shindig/gadgets/rewrite/ main/java/org/apache/shindig/gadgets/servlet/ test/java/org/apache/shindig/gadgets/servlet/

Author: etnu
Date: Wed Jun 11 02:33:22 2008
New Revision: 666593

URL: http://svn.apache.org/viewvc?rev=666593&view=rev
Log:
Applied second round of proxy refactoring for SHINDIG-371.

This is an extensive change to ProxyHandler, moving all makeRequest functionality to MakeRequestHandler.

A common base class that handles the shared functionality is introduced.

Many new tests added for both handlers. 

This introduces a security improvement for the open proxy: all proxied files will now be served as file attachments. This can be an effective deterrent for anyone using the open proxy as a phishing vector while still retaining the desired functionality.


Added:
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyBaseTest.java
Modified:
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/JavascriptTagMerger.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpServletResponseRecorder.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpTestFixture.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletTestFixture.java

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/JavascriptTagMerger.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/JavascriptTagMerger.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/JavascriptTagMerger.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/JavascriptTagMerger.java Wed Jun 11 02:33:22 2008
@@ -18,9 +18,9 @@
  */
 package org.apache.shindig.gadgets.rewrite;
 
-import org.apache.shindig.gadgets.servlet.ProxyHandler;
-import org.apache.shindig.gadgets.spec.GadgetSpec;
 import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.gadgets.servlet.ProxyBase;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
 
 import com.google.caja.lexer.HtmlTokenType;
 import com.google.caja.lexer.Token;
@@ -60,7 +60,7 @@
     // Force the mime-type to mimic browser expectation so rewriters
     // can function properly
     this.concatBase = concatBase
-        + ProxyHandler.REWRITE_MIME_TYPE_PARAM
+        + ProxyBase.REWRITE_MIME_TYPE_PARAM
         + "=text/javascript&"
         + "gadget="
         + Utf8UrlCoder.encode(spec.getUrl().toString())

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java Wed Jun 11 02:33:22 2008
@@ -281,7 +281,8 @@
     debug = getDebug(request);
     view = getView(request);
     userPrefs = getUserPrefs(request);
-    tokenString = request.getParameter(ProxyHandler.SECURITY_TOKEN_PARAM);
+    // TODO: This shouldn't be depending on MakeRequest at all.
+    tokenString = request.getParameter(MakeRequestHandler.SECURITY_TOKEN_PARAM);
     this.tokenDecoder = tokenDecoder;
   }
 }

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java?rev=666593&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java Wed Jun 11 02:33:22 2008
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.common.SecurityToken;
+import org.apache.shindig.common.SecurityTokenDecoder;
+import org.apache.shindig.common.SecurityTokenException;
+import org.apache.shindig.gadgets.FeedProcessor;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.ContentFetcherFactory;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.OAuthRequestParams;
+import org.apache.shindig.gadgets.rewrite.ContentRewriter;
+import org.apache.shindig.gadgets.spec.Auth;
+import org.apache.shindig.gadgets.spec.Preload;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handles gadgets.io.makeRequest requests.
+ *
+ * Unlike ProxyHandler, this may perform operations such as OAuth or signed fetch.
+ */
+public class MakeRequestHandler extends ProxyBase{
+  protected static final String UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >";
+  protected static final String POST_DATA_PARAM = "postData";
+  protected static final String METHOD_PARAM = "httpMethod";
+  protected static final String SECURITY_TOKEN_PARAM = "st";
+  protected static final String HEADERS_PARAM = "headers";
+  protected static final String NOCACHE_PARAM = "nocache";
+  protected static final String SIGN_VIEWER = "signViewer";
+  protected static final String SIGN_OWNER = "signOwner";
+  protected static final String CONTENT_TYPE_PARAM = "contentType";
+  protected static final String NUM_ENTRIES_PARAM = "numEntries";
+  protected static final String DEFAULT_NUM_ENTRIES = "3";
+  protected static final String GET_SUMMARIES_PARAM = "getSummaries";
+
+  private final SecurityTokenDecoder securityTokenDecoder;
+  private final ContentFetcherFactory contentFetcherFactory;
+  private final ContentRewriter rewriter;
+
+  @Inject
+  public MakeRequestHandler(ContentFetcherFactory contentFetcherFactory,
+                            SecurityTokenDecoder securityTokenDecoder,
+                            ContentRewriter rewriter) {
+    this.contentFetcherFactory = contentFetcherFactory;
+    this.securityTokenDecoder = securityTokenDecoder;
+    this.rewriter = rewriter;
+  }
+
+  /**
+   * Executes a request, returning the response as JSON to be handled by makeRequest.
+   */
+  @Override
+  public void fetch(HttpServletRequest request, HttpServletResponse response)
+      throws GadgetException, IOException {
+    HttpRequest rcr = buildHttpRequest(request);
+
+    // Figure out whether authentication is required
+    Auth auth = Auth.parse(getParameter(request, Preload.AUTHZ_ATTR, ""));
+    SecurityToken authToken = null;
+    if (auth != Auth.NONE) {
+      authToken = extractAndValidateToken(request);
+    }
+
+    // Build the chain of fetchers that will handle the request
+    HttpFetcher fetcher = getHttpFetcher(auth, authToken, request);
+
+    // Serialize the response
+    HttpResponse results = fetcher.fetch(rcr);
+
+    // Serialize the response
+    String output = convertResponseToJson(authToken, request, results);
+
+    // Find and set the refresh interval
+    setResponseHeaders(request, response, results);
+
+    response.setStatus(HttpServletResponse.SC_OK);
+    response.setContentType("application/json; charset=utf-8");
+    response.getWriter().write(UNPARSEABLE_CRUFT + output);
+  }
+
+  /**
+   * Generate a remote content request based on the parameters
+   * sent from the client.
+   * @throws GadgetException
+   */
+  private HttpRequest buildHttpRequest(HttpServletRequest request) throws GadgetException {
+    try {
+      String encoding = request.getCharacterEncoding();
+      if (encoding == null) {
+        encoding = "UTF-8";
+      }
+
+      URI url = validateUrl(request.getParameter(URL_PARAM));
+      String method = request.getMethod();
+      Map<String, List<String>> headers = null;
+      byte[] postBody = null;
+
+      method = getParameter(request, METHOD_PARAM, "GET");
+      postBody = getParameter(request, POST_DATA_PARAM, "").getBytes();
+
+      String headerData = request.getParameter(HEADERS_PARAM);
+      if (headerData == null || headerData.length() == 0) {
+        headers = Collections.emptyMap();
+      } else {
+        // We actually only accept single key value mappings now.
+        headers = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+        String[] headerList = headerData.split("&");
+        for (String header : headerList) {
+          String[] parts = header.split("=");
+          if (parts.length != 2) {
+            throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+                "Malformed header specified,");
+          }
+          headers.put(URLDecoder.decode(parts[0], encoding),
+              Arrays.asList(URLDecoder.decode(parts[1], encoding)));
+        }
+      }
+
+      removeUnsafeHeaders(headers);
+
+      HttpRequest.Options options = new HttpRequest.Options();
+      options.ignoreCache = "1".equals(request.getParameter(NOCACHE_PARAM));
+      if (request.getParameter(SIGN_VIEWER) != null) {
+        options.viewerSigned = Boolean.parseBoolean(request.getParameter(SIGN_VIEWER));
+      }
+      if (request.getParameter(SIGN_OWNER) != null) {
+        options.ownerSigned = Boolean.parseBoolean(request.getParameter(SIGN_OWNER));
+      }
+      if (request.getParameter(GADGET_PARAM) != null) {
+        options.gadgetUri = URI.create(request.getParameter(GADGET_PARAM));
+      }
+      options.rewriter = rewriter;
+
+      // Allow the rewriter to use an externally forced mime type. This is needed
+      // allows proper rewriting of <script src="x"/> where x is returned with
+      // a content type like text/html which unfortunately happens all too often
+      options.rewriteMimeType = request.getParameter(REWRITE_MIME_TYPE_PARAM);
+
+      return new HttpRequest(method, url, headers, postBody, options);
+    } catch (UnsupportedEncodingException e) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
+    }
+  }
+
+  /**
+   * Removes unsafe headers from the header set.
+   */
+  private void removeUnsafeHeaders(Map<String, List<String>> headers) {
+    // Host must be removed.
+    final String[] badHeaders = new String[] {
+        // No legitimate reason to over ride these.
+        // TODO: We probably need to test variations as well.
+        "Host", "Accept", "Accept-Encoding"
+    };
+    for (String bad : badHeaders) {
+      headers.remove(bad);
+    }
+  }
+
+  /**
+   * At the moment our fetcher chain is short.  In the future we might add
+   * additional layers for things like caching or throttling.
+   *
+   * Whatever we do needs to return a reference to the OAuthFetcher, if it is
+   * present, so we can pull data out as needed.
+   */
+  private HttpFetcher getHttpFetcher(Auth auth, SecurityToken token, HttpServletRequest request)
+      throws GadgetException {
+    switch (auth) {
+      case NONE:
+        return contentFetcherFactory.get();
+      case SIGNED:
+        return contentFetcherFactory.getSigningFetcher(token);
+      case AUTHENTICATED:
+        return contentFetcherFactory.getOAuthFetcher(token, new OAuthRequestParams(request));
+      default:
+        return contentFetcherFactory.get();
+    }
+  }
+
+  /**
+   * Format a response as JSON, including additional JSON inserted by
+   * chained content fetchers.
+   */
+  private String convertResponseToJson(SecurityToken authToken, HttpServletRequest request,
+      HttpResponse results) throws GadgetException {
+    try {
+      JSONObject resp = new JSONObject();
+      String originalUrl = request.getParameter(ProxyBase.URL_PARAM);
+      String body = results.getResponseAsString();
+      if ("FEED".equals(request.getParameter(CONTENT_TYPE_PARAM))) {
+        resp.put("body", processFeed(originalUrl, request, body));
+      } else {
+        resp.put("body", body);
+      }
+      resp.put("rc", results.getHttpStatusCode());
+
+      if (authToken != null) {
+        String updatedAuthToken = authToken.getUpdatedToken();
+        if (updatedAuthToken != null) {
+          resp.put("st", updatedAuthToken);
+        }
+      }
+
+      // Merge in additional response data
+      for (Map.Entry<String, String> entry : results.getMetadata().entrySet()) {
+        resp.put(entry.getKey(), entry.getValue());
+      }
+      // Use raw param as key as URL may have to be decoded
+      return new JSONObject().put(originalUrl, resp).toString();
+    } catch (JSONException e) {
+      return "";
+    }
+  }
+
+  /**
+   * @param request
+   * @return A valid token for the given input.
+   */
+  private SecurityToken extractAndValidateToken(HttpServletRequest request)
+      throws GadgetException {
+    String token = getParameter(request, SECURITY_TOKEN_PARAM, "");
+    try {
+      return securityTokenDecoder.createToken(token);
+    } catch (SecurityTokenException e) {
+      throw new GadgetException(GadgetException.Code.INVALID_SECURITY_TOKEN, e);
+    }
+  }
+
+  /**
+   * Processes a feed (RSS or Atom) using FeedProcessor.
+   */
+  private String processFeed(String url, HttpServletRequest req, String xml)
+      throws GadgetException {
+    boolean getSummaries = Boolean.parseBoolean(getParameter(req, GET_SUMMARIES_PARAM, "false"));
+    int numEntries = Integer.parseInt(getParameter(req, NUM_ENTRIES_PARAM, DEFAULT_NUM_ENTRIES));
+    return new FeedProcessor().process(url, xml, getSummaries, numEntries).toString();
+  }
+}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java Wed Jun 11 02:33:22 2008
@@ -41,18 +41,18 @@
 public class MakeRequestServlet extends InjectedServlet {
   private final static Logger LOG = Logger.getLogger(MakeRequestServlet.class.getName());
 
-  private ProxyHandler proxyHandler;
+  private MakeRequestHandler makeRequestHandler;
 
   @Inject
-  public void setProxyHandler(ProxyHandler proxyHandler) {
-    this.proxyHandler = proxyHandler;
+  public void setMakeRequestHandler(MakeRequestHandler makeRequestHandler) {
+    this.makeRequestHandler = makeRequestHandler;
   }
 
   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response)
       throws IOException {
     try {
-      proxyHandler.fetchJson(request, response);
+      makeRequestHandler.fetch(request, response);
     } catch (GadgetException e) {
       // TODO: Move this logic into ProxyHandler / MakeRequestHandler.
       outputError(e, response);

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java?rev=666593&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java Wed Jun 11 02:33:22 2008
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Base class for proxy-based handlers.
+ */
+public abstract class ProxyBase {
+  protected static final String URL_PARAM = "url";
+  protected static final String REFRESH_PARAM = "refresh";
+  protected static final String GADGET_PARAM = "gadget";
+
+  // Public because of rewriter. Rewriter should be cleaned up.
+  public static final String REWRITE_MIME_TYPE_PARAM = "rewriteMime";
+
+  /**
+   * Validates the given url.
+   *
+   * @return A URI representing a validated form of the url.
+   * @throws GadgetException If the url is not valid.
+   */
+  protected URI validateUrl(String urlToValidate) throws GadgetException {
+    if (urlToValidate == null) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "url parameter is missing.");
+    }
+    try {
+      URI url = new URI(urlToValidate);
+      if (!"http".equals(url.getScheme()) && !"https".equals(url.getScheme())) {
+        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+            "Invalid request url scheme; only " +
+            "\"http\" and \"https\" supported.");
+      }
+      if (url.getPath() == null || url.getPath().length() == 0) {
+        // Forcibly set the path to "/" if it is empty
+        url = new URI(url.getScheme(),
+                      url.getUserInfo(),
+                      url.getHost(),
+                      url.getPort(),
+                      "/", url.getQuery(),
+                      url.getFragment());
+      }
+      return url;
+    } catch (URISyntaxException use) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "url parameter is not a valid url.");
+    }
+  }
+
+  /**
+   * Extracts the first parameter from the parameter map with the given name.
+   *
+   * @param request The request to extract parameters from.
+   * @param name The name of the parameter to retrieve.
+   * @param defaultValue The default value to use if the parameter is not set.
+   * @return The parameter, if found, or defaultValue
+   */
+  protected String getParameter(HttpServletRequest request, String name, String defaultValue) {
+    String ret = request.getParameter(name);
+    return ret == null ? defaultValue : ret;
+  }
+
+  /**
+   * Sets cache control headers for the response.
+   */
+  protected void setResponseHeaders(HttpServletRequest request,
+      HttpServletResponse response, HttpResponse results) {
+    int refreshInterval = 0;
+    if (results.isStrictNoCache()) {
+      refreshInterval = 0;
+    } else  if (request.getParameter(REFRESH_PARAM) != null) {
+      refreshInterval =  Integer.valueOf(request.getParameter(REFRESH_PARAM));
+    } else {
+      refreshInterval = Math.max(60 * 60, (int)(results.getExpiration() / 1000));
+    }
+    HttpUtil.setCachingHeaders(response, refreshInterval);
+    response.setHeader("Content-Disposition", "attachment;filename=p.txt");
+  }
+
+  /**
+   * Processes the given request.
+   */
+  abstract public void fetch(HttpServletRequest request, HttpServletResponse response)
+      throws GadgetException, IOException;
+}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java Wed Jun 11 02:33:22 2008
@@ -18,68 +18,30 @@
  */
 package org.apache.shindig.gadgets.servlet;
 
-import org.apache.shindig.common.SecurityToken;
-import org.apache.shindig.common.SecurityTokenDecoder;
-import org.apache.shindig.common.SecurityTokenException;
-import org.apache.shindig.gadgets.FeedProcessor;
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.LockedDomainService;
-import org.apache.shindig.gadgets.http.ContentFetcherFactory;
 import org.apache.shindig.gadgets.http.HttpFetcher;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
-import org.apache.shindig.gadgets.oauth.OAuthRequestParams;
 import org.apache.shindig.gadgets.rewrite.ContentRewriter;
-import org.apache.shindig.gadgets.spec.Auth;
-import org.apache.shindig.gadgets.spec.Preload;
 
 import com.google.inject.Inject;
 
-import org.json.JSONException;
-import org.json.JSONObject;
-
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.TreeMap;
 import java.util.logging.Logger;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-public class ProxyHandler {
-  public static final String UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >";
-  public static final String POST_DATA_PARAM = "postData";
-  public static final String METHOD_PARAM = "httpMethod";
-  public static final String SECURITY_TOKEN_PARAM = "st";
-  public static final String HEADERS_PARAM = "headers";
-  public static final String NOCACHE_PARAM = "nocache";
-  public static final String REWRITE_MIME_TYPE_PARAM = "rewriteMime";
-  public static final String SIGN_VIEWER = "signViewer";
-  public static final String SIGN_OWNER = "signOwner";
-  public static final String URL_PARAM = "url";
-  public static final String REFRESH_PARAM = "refresh";
-  public static final String GADGET_PARAM = "gadget";
-  public static final String CONTENT_TYPE_PARAM = "contentType";
-  public static final String NUM_ENTRIES_PARAM = "numEntries";
-  public static final String DEFAULT_NUM_ENTRIES = "3";
-  public static final String GET_SUMMARIES_PARAM = "getSummaries";
-
+public class ProxyHandler extends ProxyBase {
   private static final Logger logger =
       Logger.getLogger(ProxyHandler.class.getPackage().getName());
 
-
-  private final SecurityTokenDecoder securityTokenDecoder;
-
   private final static Set<String> DISALLOWED_RESPONSE_HEADERS
       = new HashSet<String>();
   static {
@@ -98,257 +60,60 @@
 
   // This isn't a final field because we want to support optional injection.
   // This is a limitation of Guice, but this workaround...works.
-  private ContentFetcherFactory contentFetcherFactory;
+  private final HttpFetcher fetcher;
   private final LockedDomainService domainLocker;
   private final ContentRewriter rewriter;
 
   @Inject
-  public void setContentFetcher(ContentFetcherFactory contentFetcherFactory) {
-    this.contentFetcherFactory = contentFetcherFactory;
-  }
-
-  @Inject
-  public ProxyHandler(ContentFetcherFactory contentFetcherFactory,
-                      SecurityTokenDecoder securityTokenDecoder,
+  public ProxyHandler(HttpFetcher fetcher,
                       LockedDomainService lockedDomainService,
                       ContentRewriter rewriter) {
-    this.contentFetcherFactory = contentFetcherFactory;
-    this.securityTokenDecoder = securityTokenDecoder;
+    this.fetcher = fetcher;
     this.domainLocker = lockedDomainService;
     this.rewriter = rewriter;
   }
 
   /**
-   * Sets cache control headers for the response.
+   * Generate a remote content request based on the parameters sent from the client.
    */
-  private void setCachingHeaders(HttpServletRequest request,
-      HttpServletResponse response, HttpResponse results) {
-    int refreshInterval = 0;
-    if (results.isStrictNoCache()) {
-      refreshInterval = 0;
-    } else  if (request.getParameter(REFRESH_PARAM) != null) {
-      refreshInterval =  Integer.valueOf(request.getParameter(REFRESH_PARAM));
-    } else {
-      refreshInterval = Math.max(60 * 60, (int)(results.getExpiration() / 1000));
+  private HttpRequest buildHttpRequest(HttpServletRequest request) throws GadgetException {
+    URI url = validateUrl(request.getParameter(URL_PARAM));
+    HttpRequest.Options options = new HttpRequest.Options();
+    if (request.getParameter(GADGET_PARAM) != null) {
+      options.gadgetUri = URI.create(request.getParameter(GADGET_PARAM));
     }
-    HttpUtil.setCachingHeaders(response, refreshInterval);
-  }
-
-  /**
-   * Retrieves a file and returns it as a json response.
-   *
-   * @param request
-   * @param response
-   * @throws IOException
-   * @throws GadgetException
-   */
-  public void fetchJson(HttpServletRequest request,
-                        HttpServletResponse response)
-      throws IOException, GadgetException {
-
-    // Build up the request to make
-    HttpRequest rcr = buildHttpRequest(request);
-
-    // Figure out whether authentication is required
-    Auth auth = Auth.parse(getParameter(request, Preload.AUTHZ_ATTR, ""));
-    SecurityToken authToken = null;
-    if (auth != Auth.NONE) {
-      authToken = extractAndValidateToken(request);
-    }
-
-    // Build the chain of fetchers that will handle the request
-    HttpFetcher fetcher = getHttpFetcher(auth, authToken, request, response);
-
-    // Do the fetch
-    HttpResponse results = fetcher.fetch(rcr);
-
-    // Serialize the response
-    String output = serializeJsonResponse(authToken, request, results);
-
-    // Find and set the refresh interval
-    setCachingHeaders(request, response, results);
-
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType("application/json; charset=utf-8");
-    response.setHeader("Content-Disposition", "attachment;filename=p.txt");
-    response.getWriter().write(output);
-  }
-
-  /**
-   * Generate a remote content request based on the parameters
-   * sent from the client.
-   * @throws GadgetException
-   */
-  @SuppressWarnings("unchecked")
-  private HttpRequest buildHttpRequest(
-      HttpServletRequest request) throws GadgetException {
-    try {
-      String encoding = request.getCharacterEncoding();
-      if (encoding == null) {
-        encoding = "UTF-8";
-      }
-
-      URI url = validateUrl(request.getParameter(URL_PARAM));
-      String method = request.getMethod();
-      Map<String, List<String>> headers = null;
-      byte[] postBody = null;
-
-      if ("POST".equals(method)) {
-        method = getParameter(request, METHOD_PARAM, "GET");
-        postBody = getParameter(request, POST_DATA_PARAM, "").getBytes();
-
-        String headerData = request.getParameter(HEADERS_PARAM);
-        if (headerData == null || headerData.length() == 0) {
-          headers = Collections.emptyMap();
-        } else {
-          // We actually only accept single key value mappings now.
-          headers = new TreeMap<String, List<String>>(
-              String.CASE_INSENSITIVE_ORDER);
-          String[] headerList = headerData.split("&");
-          for (String header : headerList) {
-            String[] parts = header.split("=");
-            if (parts.length != 2) {
-              throw new GadgetException(
-                  GadgetException.Code.INTERNAL_SERVER_ERROR,
-              "malformed header specified");
-            }
-            headers.put(URLDecoder.decode(parts[0], encoding),
-                Arrays.asList(URLDecoder.decode(parts[1], encoding)));
-          }
-        }
-      } else {
-        postBody = null;
-        headers = new TreeMap<String, List<String>>(
-            String.CASE_INSENSITIVE_ORDER);
-        Enumeration<String> headerNames = request.getHeaderNames();
-        while (headerNames.hasMoreElements()) {
-          String header = headerNames.nextElement();
-          headers.put(header, Collections.list(request.getHeaders(header)));
-        }
-      }
-
-      removeUnsafeHeaders(headers);
-
-      HttpRequest.Options options =
-        new HttpRequest.Options();
-      options.ignoreCache = "1".equals(request.getParameter(NOCACHE_PARAM));
-      if (request.getParameter(SIGN_VIEWER) != null) {
-        options.viewerSigned = Boolean
-            .parseBoolean(request.getParameter(SIGN_VIEWER));
-      }
-      if (request.getParameter(SIGN_OWNER) != null) {
-        options.ownerSigned = Boolean
-            .parseBoolean(request.getParameter(SIGN_OWNER));
-      }
-      if (request.getParameter(GADGET_PARAM) != null) {
-        options.gadgetUri = URI.create(request.getParameter(GADGET_PARAM));
-      }
-      options.rewriter = rewriter;
-
-      // Allow the rewriter to use an externally forced mime type. This is needed
-      // allows proper rewriting of <script src="x"/> where x is returned with
-      // a content type like text/html which unfortunately happens all too often
-      options.rewriteMimeType = request.getParameter(REWRITE_MIME_TYPE_PARAM);
-
-      return new HttpRequest(
-          method, url, headers, postBody, options);
-    } catch (UnsupportedEncodingException e) {
-      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
-    }
-  }
-
-  /**
-   * At the moment our fetcher chain is short.  In the future we might add
-   * additional layers for things like caching or throttling.
-   *
-   * Whatever we do needs to return a reference to the OAuthFetcher, if it is
-   * present, so we can pull data out as needed.
-   */
-  private HttpFetcher getHttpFetcher(Auth auth, SecurityToken authToken,
-      HttpServletRequest request, HttpServletResponse response)
-      throws GadgetException {
-    switch (auth) {
-      case NONE:
-        return contentFetcherFactory.get();
-      case SIGNED:
-        return contentFetcherFactory.getSigningFetcher(authToken);
-      case AUTHENTICATED:
-        return contentFetcherFactory.getOAuthFetcher(
-            authToken, new OAuthRequestParams(request));
-      default:
-        return contentFetcherFactory.get();
-    }
-  }
-
-  /**
-   * Format a response as JSON, including additional JSON inserted by
-   * chained content fetchers.
-   */
-  private String serializeJsonResponse(SecurityToken authToken,
-      HttpServletRequest request, HttpResponse results) throws GadgetException {
-    try {
-      JSONObject resp = new JSONObject();
-      String originalUrl = request.getParameter(URL_PARAM);
-      String body = results.getResponseAsString();
-      if ("FEED".equals(request.getParameter(CONTENT_TYPE_PARAM))) {
-        resp.put("body", processFeed(originalUrl, request, body));
-      } else {
-        resp.put("body", body);
-      }
-      resp.put("rc", results.getHttpStatusCode());
+    options.rewriter = rewriter;
 
-      if (authToken != null) {
-        String updatedAuthToken = authToken.getUpdatedToken();
-        if (updatedAuthToken != null) {
-          resp.put("st", updatedAuthToken);
-        }
-      }
+    // Allow the rewriter to use an externally forced mime type. This is needed
+    // allows proper rewriting of <script src="x"/> where x is returned with
+    // a content type like text/html which unfortunately happens all too often
+    options.rewriteMimeType = request.getParameter(REWRITE_MIME_TYPE_PARAM);
 
-      // Merge in additional response data
-      for (Map.Entry<String, String> entry : results.getMetadata().entrySet()) {
-        resp.put(entry.getKey(), entry.getValue());
-      }
-      // Use raw param as key as URL may have to be decoded
-      JSONObject json = new JSONObject().put(originalUrl, resp);
-      return UNPARSEABLE_CRUFT + json.toString();
-    } catch (JSONException e) {
-      return "";
-    }
+    return new HttpRequest(url, options);
   }
 
-  /**
-   * This is called for embedding images inline in gadgets, e.g. via img src
-   * links created with IG_Embed
-   *
-   * @param request
-   * @param response
-   * @throws IOException
-   * @throws GadgetException
-   */
-  public void fetch(HttpServletRequest request,
-                    HttpServletResponse response)
+  @Override
+  public void fetch(HttpServletRequest request, HttpServletResponse response)
       throws IOException, GadgetException {
+    if (request.getHeader("If-Modified-Since") != null) {
+      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+      return;
+    }
 
     String host = request.getHeader("Host");
     if (!domainLocker.embedCanRender(host)) {
       // Force embedded images and the like to their own domain to avoid XSS
       // in gadget domains.
-      String msg = "Embed request for url " +
-          getParameter(request, URL_PARAM, "") +
+      String msg = "Embed request for url " + getParameter(request, URL_PARAM, "") +
           " made to wrong domain " + host;
       logger.info(msg);
       throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, msg);
     }
 
-    if (request.getHeader("If-Modified-Since") != null) {
-      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
-      return;
-    }
-
     HttpRequest rcr = buildHttpRequest(request);
-    HttpResponse results = contentFetcherFactory.get().fetch(rcr);
+    HttpResponse results = fetcher.fetch(rcr);
 
-    setCachingHeaders(request, response, results);
+    setResponseHeaders(request, response, results);
 
     for (Map.Entry<String, List<String>> entry : results.getAllHeaders().entrySet()) {
       String name = entry.getKey();
@@ -369,93 +134,4 @@
 
     response.getOutputStream().write(results.getResponseAsBytes());
   }
-
-  /**
-   * Removes unsafe headers from the header set.
-   * @param headers
-   */
-  private void removeUnsafeHeaders(Map<String, List<String>> headers) {
-    // Host must be removed.
-    final String[] badHeaders = new String[] {
-        // No legitimate reason to over ride these.
-        // TODO: We probably need to test variations as well.
-        "Host", "Accept", "Accept-Encoding"
-    };
-    for (String bad : badHeaders) {
-      headers.remove(bad);
-    }
-  }
-
-  /**
-   * Validates that the given url is valid for this request.
-   *
-   * @param urlToValidate
-   * @return A URL object of the URL
-   * @throws GadgetException if the URL fails security checks or is malformed.
-   */
-  public URI validateUrl(String urlToValidate) throws GadgetException {
-    if (urlToValidate == null) {
-      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
-          "url parameter is missing.");
-    }
-    try {
-      URI url = new URI(urlToValidate);
-      if (!"http".equals(url.getScheme()) && !"https".equals(url.getScheme())) {
-        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
-            "Invalid request url scheme; only " +
-            "\"http\" and \"https\" supported.");
-      }
-      if (url.getPath() == null || url.getPath().length() == 0) {
-        // Forcibly set the path to "/" if it is empty
-        url = new URI(url.getScheme(),
-                      url.getUserInfo(),
-                      url.getHost(),
-                      url.getPort(),
-                      "/", url.getQuery(),
-                      url.getFragment());
-      }
-      return url;
-    } catch (URISyntaxException use) {
-      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
-          "url parameter is not a valid url.");
-    }
-  }
-
-  /**
-   * @param request
-   * @return A valid token for the given input.
-   */
-  private SecurityToken extractAndValidateToken(HttpServletRequest request)
-      throws GadgetException {
-    String token = getParameter(request, SECURITY_TOKEN_PARAM, "");
-    try {
-      return securityTokenDecoder.createToken(token);
-    } catch (SecurityTokenException e) {
-      throw new GadgetException(GadgetException.Code.INVALID_SECURITY_TOKEN, e);
-    }
-  }
-
-  /**
-   * Processes a feed (RSS or Atom) using FeedProcessor.
-   */
-  private String processFeed(String url, HttpServletRequest req, String xml)
-      throws GadgetException {
-    boolean getSummaries = Boolean.parseBoolean(
-        getParameter(req, GET_SUMMARIES_PARAM, "false"));
-    int numEntries = Integer.parseInt(
-        getParameter(req, NUM_ENTRIES_PARAM, DEFAULT_NUM_ENTRIES));
-    return new FeedProcessor().process(url, xml, getSummaries, numEntries).toString();
-  }
-
-  /**
-   * Extracts the first parameter from the parameter map with the given name.
-   * @param request
-   * @param name
-   * @return The parameter, if found, or defaultValue
-   */
-  private static String getParameter(HttpServletRequest request,
-                                     String name, String defaultValue) {
-    String ret = request.getParameter(name);
-    return ret == null ? defaultValue : ret;
-  }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpServletResponseRecorder.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpServletResponseRecorder.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpServletResponseRecorder.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpServletResponseRecorder.java Wed Jun 11 02:33:22 2008
@@ -18,9 +18,14 @@
  */
 package org.apache.shindig.gadgets.servlet;
 
+import org.apache.shindig.common.util.DateUtil;
+
+import com.google.common.collect.Maps;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.Map;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
@@ -32,6 +37,7 @@
 public class HttpServletResponseRecorder extends HttpServletResponseWrapper {
   private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
   private final PrintWriter writer = new PrintWriter(baos);
+  private final Map<String, String> headers = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
   private int httpStatusCode = 200;
 
   public HttpServletResponseRecorder(HttpServletResponse response) {
@@ -55,6 +61,10 @@
     return httpStatusCode;
   }
 
+  public String getHeader(String name) {
+    return headers.get(name);
+  }
+
   @Override
   public PrintWriter getWriter() {
     return writer;
@@ -71,6 +81,26 @@
   }
 
   @Override
+  public void addHeader(String name, String value) {
+    headers.put(name, value);
+  }
+
+  @Override
+  public void setHeader(String name, String value) {
+    addHeader(name, value);
+  }
+
+  @Override
+  public void addDateHeader(String name, long date) {
+    headers.put(name, DateUtil.formatDate(date));
+  }
+
+  @Override
+  public void setDateHeader(String name, long date) {
+    addDateHeader(name, date);
+  }
+
+  @Override
   public void setStatus(int httpStatusCode) {
     this.httpStatusCode = httpStatusCode;
   }

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpTestFixture.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpTestFixture.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpTestFixture.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpTestFixture.java Wed Jun 11 02:33:22 2008
@@ -25,7 +25,6 @@
 import org.apache.shindig.gadgets.rewrite.NoOpContentRewriter;
 
 public abstract class HttpTestFixture extends GadgetTestFixture {
-  public final ProxyHandler proxyHandler;
   public final GadgetRenderingTask gadgetRenderer;
   public final JsonRpcHandler jsonRpcHandler;
   public final ContentFetcherFactory contentFetcherFactory
@@ -37,11 +36,6 @@
 
   public HttpTestFixture() {
     super();
-    proxyHandler = new ProxyHandler(
-        contentFetcherFactory,
-        securityTokenDecoder,
-        lockedDomainService,
-        rewriter);
     gadgetRenderer = new GadgetRenderingTask(gadgetServer, bundleFactory,
         registry, containerConfig, urlGenerator, securityTokenDecoder, lockedDomainService);
     jsonRpcHandler = new JsonRpcHandler(executor, gadgetServer, urlGenerator);

Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java?rev=666593&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java Wed Jun 11 02:33:22 2008
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static junitx.framework.StringAssert.assertStartsWith;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.SecurityToken;
+import org.apache.shindig.common.SecurityTokenException;
+import org.apache.shindig.gadgets.FakeGadgetToken;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.spec.Auth;
+import org.apache.shindig.gadgets.spec.Preload;
+
+import com.google.common.collect.Lists;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for MakeRequestHandler.
+ */
+public class MakeRequestHandlerTest {
+  private static final String REQUEST_URL = "http://example.org/file";
+  private static final String REQUEST_BODY = "I+am+the+request+body!foo=baz%20la";
+  private static final String RESPONSE_BODY = "makeRequest response body";
+  private static final SecurityToken DUMMY_TOKEN = new FakeGadgetToken();
+
+  private final ServletTestFixture fixture = new ServletTestFixture();
+  private final MakeRequestHandler handler = new MakeRequestHandler(fixture.contentFetcherFactory,
+      fixture.securityTokenDecoder, fixture.rewriter);
+  private final HttpServletResponseRecorder recorder
+      = new HttpServletResponseRecorder(fixture.response);
+
+  private void expectGetAndReturnBody(String response) throws Exception {
+    expectGetAndReturnBody(fixture.httpFetcher, response);
+  }
+
+  private void expectGetAndReturnBody(HttpFetcher fetcher, String response) throws Exception {
+    HttpRequest request = HttpRequest.getRequest(URI.create(REQUEST_URL), false);
+    expect(fetcher.fetch(request)).andReturn(new HttpResponse(response));
+  }
+
+  private void expectPostAndReturnBody(String postData, String response) throws Exception {
+    expectPostAndReturnBody(fixture.httpFetcher, postData, response);
+  }
+
+  private void expectPostAndReturnBody(HttpFetcher fetcher, String postData, String response)
+      throws Exception {
+    HttpRequest request = new HttpRequest(URI.create(REQUEST_URL), REQUEST_BODY.getBytes("UTF-8"));
+    expect(fixture.request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("POST");
+    expect(fixture.request.getParameter(MakeRequestHandler.POST_DATA_PARAM))
+        .andReturn(REQUEST_BODY);
+    expect(fetcher.fetch(request)).andReturn(new HttpResponse(response));
+  }
+
+  private JSONObject extractJsonFromResponse() throws JSONException {
+    String body = recorder.getResponseAsString();
+    assertStartsWith(MakeRequestHandler.UNPARSEABLE_CRUFT, body);
+    body = body.substring(MakeRequestHandler.UNPARSEABLE_CRUFT.length());
+    return new JSONObject(body).getJSONObject(REQUEST_URL);
+  }
+
+  @Before
+  public void setUp() {
+    expect(fixture.request.getMethod()).andReturn("POST").anyTimes();
+    expect(fixture.request.getParameter(MakeRequestHandler.URL_PARAM))
+        .andReturn(REQUEST_URL).anyTimes();
+  }
+
+  @Test
+  public void getRequest() throws Exception {
+    expectGetAndReturnBody(RESPONSE_BODY);
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+
+    JSONObject results = extractJsonFromResponse();
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+  }
+
+  @Test
+  public void explicitHeaders() throws Exception {
+    String headerString = "X-Foo=bar&X-Bar=baz%20foo";
+
+    final List<HttpRequest> requests = Lists.newArrayList();
+    HttpFetcher fakeFetcher = new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) {
+        requests.add(request);
+        return new HttpResponse(RESPONSE_BODY);
+      }
+    };
+
+    fixture.reset();
+    setUp();
+    expect(fixture.contentFetcherFactory.get()).andReturn(fakeFetcher);
+    expect(fixture.request.getParameter(MakeRequestHandler.HEADERS_PARAM)).andReturn(headerString);
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    fixture.verify();
+
+    assertEquals("bar", requests.get(0).getHeader("X-Foo"));
+    assertEquals("baz foo", requests.get(0).getHeader("X-Bar"));
+
+    JSONObject results = extractJsonFromResponse();
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+  }
+
+  @Test
+  public void postRequest() throws Exception {
+    expectPostAndReturnBody(REQUEST_BODY, RESPONSE_BODY);
+    expect(fixture.request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("POST");
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+  }
+
+  @Test
+  public void fetchContentTypeFeed() throws Exception {
+    String entryTitle = "Feed title";
+    String entryLink = "http://example.org/entry/0/1";
+    String entrySummary = "This is the summary";
+    String rss = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                 "<rss version=\"2.0\"><channel>" +
+                 "<title>dummy</title>" +
+                 "<link>http://example.org/</link>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "</channel></rss>";
+
+    expectGetAndReturnBody(rss);
+    expect(fixture.request.getParameter(MakeRequestHandler.CONTENT_TYPE_PARAM)).andReturn("FEED");
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    JSONObject feed = new JSONObject(results.getString("body"));
+    JSONObject entry = feed.getJSONArray("Entry").getJSONObject(0);
+
+    assertEquals(entryTitle, entry.getString("Title"));
+    assertEquals(entryLink, entry.getString("Link"));
+    assertNull("getSummaries has the wrong default value (should be false).",
+        entry.optString("Summary", null));
+  }
+
+  @Test
+  public void fetchFeedWithParameters() throws Exception {
+    String entryTitle = "Feed title";
+    String entryLink = "http://example.org/entry/0/1";
+    String entrySummary = "This is the summary";
+    String rss = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                 "<rss version=\"2.0\"><channel>" +
+                 "<title>dummy</title>" +
+                 "<link>http://example.org/</link>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "</channel></rss>";
+
+    expectGetAndReturnBody(rss);
+    expect(fixture.request.getParameter(MakeRequestHandler.GET_SUMMARIES_PARAM)).andReturn("true");
+    expect(fixture.request.getParameter(MakeRequestHandler.NUM_ENTRIES_PARAM)).andReturn("2");
+    expect(fixture.request.getParameter(MakeRequestHandler.CONTENT_TYPE_PARAM)).andReturn("FEED");
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    JSONObject feed = new JSONObject(results.getString("body"));
+    JSONArray feeds = feed.getJSONArray("Entry");
+
+    assertEquals("numEntries not parsed correctly.", 2, feeds.length());
+
+    JSONObject entry = feeds.getJSONObject(1);
+    assertEquals(entryTitle, entry.getString("Title"));
+    assertEquals(entryLink, entry.getString("Link"));
+    assertTrue("getSummaries not parsed correctly.", entry.has("Summary"));
+    assertEquals(entrySummary, entry.getString("Summary"));
+  }
+
+  @Test
+  public void fetchEmptyDocument() throws Exception {
+    expectGetAndReturnBody("");
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals("", results.get("body"));
+  }
+
+  @Test
+  public void signedGetRequest() throws Exception {
+    // Doesn't actually sign since it returns the standard fetcher.
+    // Signing tests are in SigningFetcherTest
+    expect(fixture.securityTokenDecoder.createToken("fake-token")).andReturn(DUMMY_TOKEN);
+    expect(fixture.request.getParameter(MakeRequestHandler.SECURITY_TOKEN_PARAM))
+        .andReturn("fake-token").atLeastOnce();
+    expect(fixture.request.getParameter(Preload.AUTHZ_ATTR))
+        .andReturn(Auth.SIGNED.toString()).atLeastOnce();
+    expect(fixture.signingFetcher.fetch(isA(HttpRequest.class)))
+        .andReturn(new HttpResponse(RESPONSE_BODY));
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.get("body"));
+  }
+
+  @Test
+  public void signedPostRequest() throws Exception {
+    // Doesn't actually sign since it returns the standard fetcher.
+    // Signing tests are in SigningFetcherTest
+    expectPostAndReturnBody(fixture.signingFetcher, REQUEST_BODY, RESPONSE_BODY);
+    expect(fixture.securityTokenDecoder.createToken("fake-token")).andReturn(DUMMY_TOKEN);
+    expect(fixture.request.getParameter(MakeRequestHandler.SECURITY_TOKEN_PARAM))
+        .andReturn("fake-token").atLeastOnce();
+    expect(fixture.request.getParameter(Preload.AUTHZ_ATTR))
+        .andReturn(Auth.SIGNED.toString()).atLeastOnce();
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertFalse("A security token was returned when it was not requested.",
+        results.has("st"));
+  }
+
+  @Test
+  public void changeSecurityToken() throws Exception {
+    // Doesn't actually sign since it returns the standard fetcher.
+    // Signing tests are in SigningFetcherTest
+    expectGetAndReturnBody(fixture.signingFetcher, RESPONSE_BODY);
+    FakeGadgetToken authToken = new FakeGadgetToken("updated");
+    expect(fixture.securityTokenDecoder.createToken("fake-token")).andReturn(authToken);
+    expect(fixture.request.getParameter(MakeRequestHandler.SECURITY_TOKEN_PARAM))
+        .andReturn("fake-token").atLeastOnce();
+    expect(fixture.request.getParameter(Preload.AUTHZ_ATTR))
+        .andReturn(Auth.SIGNED.toString()).atLeastOnce();
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertEquals("updated", results.getString("st"));
+  }
+
+  @Test
+  public void doOAuthRequest() throws Exception {
+    // Doesn't actually do oauth dance since it returns the standard fetcher.
+    // OAuth tests are in OAuthFetcherTest
+    expectGetAndReturnBody(fixture.oauthFetcher, RESPONSE_BODY);
+    FakeGadgetToken authToken = new FakeGadgetToken("updated");
+    expect(fixture.securityTokenDecoder.createToken("fake-token")).andReturn(authToken);
+    expect(fixture.request.getParameter(MakeRequestHandler.SECURITY_TOKEN_PARAM))
+        .andReturn("fake-token").atLeastOnce();
+    expect(fixture.request.getParameter(Preload.AUTHZ_ATTR))
+        .andReturn(Auth.AUTHENTICATED.toString()).atLeastOnce();
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+  }
+
+  @Test
+  public void invalidSigningTypeTreatedAsNone() throws Exception {
+    expectGetAndReturnBody(RESPONSE_BODY);
+    expect(fixture.request.getParameter(Preload.AUTHZ_ATTR)).andReturn("garbage");
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+  }
+
+  @Test
+  public void badHttpResponseIsPropagated() throws Exception {
+    HttpRequest request = HttpRequest.getRequest(URI.create(REQUEST_URL), false);
+    expect(fixture.httpFetcher.fetch(request)).andReturn(HttpResponse.error());
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_INTERNAL_SERVER_ERROR, results.getInt("rc"));
+  }
+
+  @Test(expected = GadgetException.class)
+  public void badSecurityTokenThrows() throws Exception {
+    expect(fixture.request.getParameter(MakeRequestHandler.SECURITY_TOKEN_PARAM))
+        .andReturn("fake-token").atLeastOnce();
+    expect(fixture.request.getParameter(Preload.AUTHZ_ATTR))
+        .andReturn(Auth.SIGNED.toString()).atLeastOnce();
+    expect(fixture.securityTokenDecoder.createToken("fake-token"))
+        .andThrow(new SecurityTokenException("No!"));
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+
+    assertTrue("Response for bad tokens should not be SC_OK",
+        HttpServletResponse.SC_OK != recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void metadataCopied() throws Exception {
+    HttpRequest request = HttpRequest.getRequest(URI.create(REQUEST_URL), false);
+    HttpResponse response = new HttpResponse("foo");
+    response.getMetadata().put("foo", RESPONSE_BODY);
+    expect(fixture.httpFetcher.fetch(request)).andReturn(response);
+    fixture.replay();
+
+    handler.fetch(fixture.request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.getString("foo"));
+  }
+}

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java?rev=666593&r1=666592&r2=666593&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java Wed Jun 11 02:33:22 2008
@@ -42,7 +42,7 @@
 /**
  * Tests for MakeRequestServlet.
  *
- * Tests are trivial; real tests are in ProxyHandlerTest.
+ * Tests are trivial; real tests are in MakeRequestHandlerTest.
  */
 public class MakeRequestServletTest {
   private static final String REQUEST_URL = "http://example.org/file";
@@ -53,6 +53,8 @@
 
   private final ServletTestFixture fixture = new ServletTestFixture();
   private final MakeRequestServlet servlet = new MakeRequestServlet();
+  private final MakeRequestHandler handler = new MakeRequestHandler(fixture.contentFetcherFactory,
+      fixture.securityTokenDecoder, fixture.rewriter);
   private final HttpServletResponseRecorder recorder
       = new HttpServletResponseRecorder(fixture.response);
   private final HttpRequest request = HttpRequest.getRequest(URI.create(REQUEST_URL), false);
@@ -60,11 +62,11 @@
 
   @Before
   public void setUp() {
-    servlet.setProxyHandler(fixture.proxyHandler);
+    servlet.setMakeRequestHandler(handler);
     expect(fixture.request.getHeaderNames()).andReturn(EMPTY_ENUM).anyTimes();
-    expect(fixture.request.getParameter(ProxyHandler.METHOD_PARAM))
+    expect(fixture.request.getParameter(MakeRequestHandler.METHOD_PARAM))
         .andReturn("GET").anyTimes();
-    expect(fixture.request.getParameter(ProxyHandler.URL_PARAM)).andReturn(REQUEST_URL).anyTimes();
+    expect(fixture.request.getParameter(ProxyBase.URL_PARAM)).andReturn(REQUEST_URL).anyTimes();
   }
 
   private void setupGet() {
@@ -78,8 +80,8 @@
   private void assertResponseOk(int expectedStatus, String expectedBody) throws JSONException {
     if (recorder.getHttpStatusCode() == HttpServletResponse.SC_OK) {
       String body = recorder.getResponseAsString();
-      assertStartsWith(ProxyHandler.UNPARSEABLE_CRUFT, body);
-      body = body.substring(ProxyHandler.UNPARSEABLE_CRUFT.length());
+      assertStartsWith(MakeRequestHandler.UNPARSEABLE_CRUFT, body);
+      body = body.substring(MakeRequestHandler.UNPARSEABLE_CRUFT.length());
       JSONObject object = new JSONObject(body);
       object = object.getJSONObject(REQUEST_URL);
       assertEquals(expectedStatus, object.getInt("rc"));

Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyBaseTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyBaseTest.java?rev=666593&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyBaseTest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyBaseTest.java Wed Jun 11 02:33:22 2008
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static junitx.framework.StringAssert.assertContains;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import com.google.common.collect.Maps;
+
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for ProxyBase.
+ */
+public class ProxyBaseTest {
+
+  private final ServletTestFixture fixture = new ServletTestFixture();
+
+  private final ProxyBase proxy = new ProxyBase() {
+    @Override
+    public void fetch(HttpServletRequest request, HttpServletResponse response) {
+      // Nothing.
+    }
+  };
+
+  @Test
+  public void validateUrlNoPath() throws Exception {
+    URI url = proxy.validateUrl("http://www.example.com");
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(-1, url.getPort());
+    assertEquals("/", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void validateUrlHttps() throws Exception {
+    URI url = proxy.validateUrl("https://www.example.com");
+    assertEquals("https", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(-1, url.getPort());
+    assertEquals("/", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void validateUrlWithPath() throws Exception {
+    URI url = proxy.validateUrl("http://www.example.com/foo");
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(-1, url.getPort());
+    assertEquals("/foo", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void validateUrlWithPort() throws Exception {
+    URI url = proxy.validateUrl("http://www.example.com:8080/foo");
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(8080, url.getPort());
+    assertEquals("/foo", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void validateUrlWithEncodedPath() throws Exception {
+    URI url = proxy.validateUrl("http://www.example.com:8080/foo%20bar");
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(8080, url.getPort());
+    assertEquals("/foo%20bar", url.getRawPath());
+    assertEquals("/foo bar", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void validateUrlWithEncodedQuery() throws Exception {
+    URI url = proxy.validateUrl("http://www.example.com:8080/foo?q=with%20space");
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(8080, url.getPort());
+    assertEquals("/foo", url.getPath());
+    assertEquals("q=with%20space", url.getRawQuery());
+    assertEquals("q=with space", url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void validateUrlWithNoPathAndEncodedQuery() throws Exception {
+    URI url = proxy.validateUrl("http://www.example.com?q=with%20space");
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getHost());
+    assertEquals(-1, url.getPort());
+    assertEquals("/", url.getPath());
+    assertEquals("q=with%20space", url.getRawQuery());
+    assertEquals("q=with space", url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void validateUrlNullInput() throws GadgetException {
+    proxy.validateUrl(null);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void validateUrlBadInput() throws GadgetException {
+    proxy.validateUrl("%$#%#$%#$%");
+  }
+
+  @Test(expected = GadgetException.class)
+  public void validateUrlBadProtocol() throws GadgetException {
+    proxy.validateUrl("gopher://foo");
+  }
+
+  @Test
+  public void setResponseHeaders() {
+    HttpResponse results = new HttpResponse(HttpResponse.SC_OK);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(fixture.response);
+    fixture.replay();
+
+    proxy.setResponseHeaders(fixture.request, recorder, results);
+
+    // Just verify that they were set. Specific values are configurable.
+    assertNotNull("Expires header not set", recorder.getHeader("Expires"));
+    assertNotNull("Cache-Control header not set", recorder.getHeader("Cache-Control"));
+    assertEquals("attachment;filename=p.txt", recorder.getHeader("Content-Disposition"));
+  }
+
+  @Test
+  public void setResponseHeadersNoCache() {
+    Map<String, List<String>> headers = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+    headers.put("Pragma", Arrays.asList("no-cache"));
+    HttpResponse results = new HttpResponse(HttpResponse.SC_OK, new byte[0], headers);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(fixture.response);
+    fixture.replay();
+
+    proxy.setResponseHeaders(fixture.request, recorder, results);
+
+    // Just verify that they were set. Specific values are configurable.
+    assertNotNull("Expires header not set", recorder.getHeader("Expires"));
+    assertEquals("no-cache", recorder.getHeader("Pragma"));
+    assertEquals("no-cache", recorder.getHeader("Cache-Control"));
+    assertEquals("attachment;filename=p.txt", recorder.getHeader("Content-Disposition"));
+  }
+
+  @Test
+  public void setResponseHeadersForceParam() {
+    HttpResponse results = new HttpResponse(HttpResponse.SC_OK);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(fixture.response);
+    expect(fixture.request.getParameter(ProxyBase.REFRESH_PARAM)).andReturn("30").anyTimes();
+    fixture.replay();
+
+    proxy.setResponseHeaders(fixture.request, recorder, results);
+
+    // Just verify that they were set. Specific values are configurable.
+    assertNotNull("Expires header not set", recorder.getHeader("Expires"));
+    assertContains("30", recorder.getHeader("Cache-Control"));
+    assertEquals("attachment;filename=p.txt", recorder.getHeader("Content-Disposition"));
+  }
+
+  @Test
+  public void getParameter() {
+    expect(fixture.request.getParameter("foo")).andReturn("bar");
+    fixture.replay();
+
+    assertEquals("bar", proxy.getParameter(fixture.request, "foo", "not foo"));
+  }
+
+  @Test
+  public void getParameterWithNullValue() {
+    expect(fixture.request.getParameter("foo")).andReturn(null);
+    fixture.replay();
+
+    assertEquals("not foo", proxy.getParameter(fixture.request, "foo", "not foo"));
+  }
+}