All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.shindig.gadgets.servlet.MakeRequestHandler Maven / Gradle / Ivy

Go to download

Renders gadgets, provides the gadget metadata service, and serves all javascript required by the OpenSocial specification.

The newest version!
/*
 * 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 java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.shindig.auth.AuthInfoUtil;
import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.JsonSerializer;
import org.apache.shindig.common.servlet.HttpUtil;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.common.util.Utf8UrlCoder;
import org.apache.shindig.config.ContainerConfig;
import org.apache.shindig.gadgets.AuthType;
import org.apache.shindig.gadgets.FeedProcessor;
import org.apache.shindig.gadgets.FetchResponseUtils;
import org.apache.shindig.gadgets.Gadget;
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.GadgetException.Code;
import org.apache.shindig.gadgets.LockedDomainService;
import org.apache.shindig.gadgets.admin.GadgetAdminStore;
import org.apache.shindig.gadgets.http.HttpRequest;
import org.apache.shindig.gadgets.http.HttpResponse;
import org.apache.shindig.gadgets.http.RequestPipeline;
import org.apache.shindig.gadgets.oauth.OAuthArguments;
import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
import org.apache.shindig.gadgets.process.ProcessingException;
import org.apache.shindig.gadgets.process.Processor;
import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
import org.apache.shindig.gadgets.rewrite.RewritingException;
import org.apache.shindig.gadgets.uri.UriCommon;
import org.apache.shindig.gadgets.uri.UriCommon.Param;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

/**
 * Handles gadgets.io.makeRequest requests.
 * 
 * Unlike ProxyHandler, this may perform operations such as OAuth or signed fetch.
 */
@Singleton
public class MakeRequestHandler {
  // Relaxed visibility for ease of integration. Try to avoid relying on these.
  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 HEADERS_PARAM = "headers";
  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 static final String GET_FULL_HEADERS_PARAM = "getFullHeaders";
  public static final String AUTHZ_PARAM = "authz";

  private final RequestPipeline requestPipeline;
  private final ResponseRewriterRegistry contentRewriterRegistry;
  private final Provider feedProcessorProvider;
  private final GadgetAdminStore gadgetAdminStore;
  private final Processor processor;
  private final LockedDomainService lockedDomainService;

  @Inject
  public MakeRequestHandler(
          RequestPipeline requestPipeline,
          @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT) ResponseRewriterRegistry contentRewriterRegistry,
          Provider feedProcessorProvider, GadgetAdminStore gadgetAdminStore,
          Processor processor, LockedDomainService lockedDomainService) {

    this.requestPipeline = requestPipeline;
    this.contentRewriterRegistry = contentRewriterRegistry;
    this.feedProcessorProvider = feedProcessorProvider;
    this.gadgetAdminStore = gadgetAdminStore;
    this.processor = processor;
    this.lockedDomainService = lockedDomainService;
  }

  /**
   * Executes a request, returning the response as JSON to be handled by makeRequest.
   */
  public void fetch(HttpServletRequest request, HttpServletResponse response)
          throws GadgetException, IOException {
    HttpRequest rcr = buildHttpRequest(request);
    String container = rcr.getContainer();

    final Uri gadgetUri = rcr.getGadget();
    if (gadgetUri == null) {
      throw new GadgetException(GadgetException.Code.MISSING_PARAMETER,
              "Unable to find gadget in request", HttpResponse.SC_FORBIDDEN);
    }

    Gadget gadget;
    GadgetContext context = new HttpGadgetContext(request) {
      public Uri getUrl() {
        return gadgetUri;
      }

      public boolean getIgnoreCache() {
        return getParameter("bypassSpecCache").equals("1");
      }
    };
    try {
      gadget = processor.process(context);
    } catch (ProcessingException e) {
      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
              "Error processing gadget", e, HttpResponse.SC_BAD_REQUEST);
    }

    // Validate gadget is correct for the host.
    // Ensures that the gadget has not hand crafted this request to represent itself as
    // another gadget in a locked domain environment.
    if (!lockedDomainService.isGadgetValidForHost(context.getHost(), gadget, container)) {
      throw new GadgetException(GadgetException.Code.GADGET_HOST_MISMATCH,
              "The gadget is incorrect for this request", HttpResponse.SC_FORBIDDEN);
    }

    if (!gadgetAdminStore.isWhitelisted(container, gadgetUri.toString())) {
      throw new GadgetException(GadgetException.Code.NON_WHITELISTED_GADGET,
              "The requested content is unavailable", HttpResponse.SC_FORBIDDEN);
    }

    // Serialize the response
    HttpResponse results = requestPipeline.execute(rcr);

    // Rewrite the response
    if (contentRewriterRegistry != null) {
      try {
        results = contentRewriterRegistry.rewriteHttpResponse(rcr, results);
      } catch (RewritingException e) {
        throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e,
                e.getHttpStatusCode());
      }
    }

    // Serialize the response
    String output = convertResponseToJson(rcr.getSecurityToken(), request, results);

    // Find and set the refresh interval
    setResponseHeaders(request, response, results);

    response.setStatus(HttpServletResponse.SC_OK);
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    response.getWriter().write(UNPARSEABLE_CRUFT + output);
  }

  /**
   * Generate a remote content request based on the parameters sent from the client.
   * 
   * @throws GadgetException
   */
  protected HttpRequest buildHttpRequest(HttpServletRequest request) throws GadgetException {
    String urlStr = request.getParameter(Param.URL.getKey());
    if (urlStr == null) {
      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, Param.URL.getKey()
              + " parameter is missing.", HttpResponse.SC_BAD_REQUEST);
    }

    Uri url;
    try {
      url = ServletUtil.validateUrl(Uri.parse(urlStr));
    } catch (IllegalArgumentException e) {
      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, "Invalid "
              + Param.URL.getKey() + " parameter", HttpResponse.SC_BAD_REQUEST);
    }

    HttpRequest req = new HttpRequest(url).setMethod(getParameter(request, METHOD_PARAM, "GET"))
            .setContainer(getContainer(request));

    setPostData(request, req);

    String headerData = getParameter(request, HEADERS_PARAM, "");
    if (headerData.length() > 0) {
      String[] headerList = StringUtils.split(headerData, '&');
      for (String header : headerList) {
        String[] parts = StringUtils.splitPreserveAllTokens(header, '=');
        if (parts.length != 2) {
          throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
                  "Malformed header param specified:" + header, HttpResponse.SC_BAD_REQUEST);
        }
        String headerName = Utf8UrlCoder.decode(parts[0]);
        if (!HttpRequestHandler.BAD_HEADERS.contains(headerName.toUpperCase())) {
          req.addHeader(headerName, Utf8UrlCoder.decode(parts[1]));
        }
      }
    }

    // Set the default content type for post requests when a content type is not specified
    if ("POST".equals(req.getMethod()) && req.getHeader("Content-Type") == null) {
      req.addHeader("Content-Type", "application/x-www-form-urlencoded");
    }

    req.setIgnoreCache("1".equals(request.getParameter(Param.NO_CACHE.getKey())));

    if (request.getParameter(Param.GADGET.getKey()) != null) {
      req.setGadget(Uri.parse(request.getParameter(Param.GADGET.getKey())));
    }

    // If the proxy request specifies a refresh param then we want to force the min TTL for
    // the retrieved entry in the cache regardless of the headers on the content when it
    // is fetched from the original source.
    if (request.getParameter(Param.REFRESH.getKey()) != null) {
      try {
        req.setCacheTtl(Integer.parseInt(request.getParameter(Param.REFRESH.getKey())));
      } catch (NumberFormatException nfe) {
        // Ignore
      }
    }
    // Allow the rewriter to use an externally forced mime type. This is needed
    // allows proper rewriting of