/* * 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.auth.AuthInfo; import org.apache.shindig.auth.SecurityToken; import org.apache.shindig.common.uri.Uri; import org.apache.shindig.common.util.Utf8UrlCoder; import org.apache.shindig.gadgets.AuthType; import org.apache.shindig.gadgets.FeedProcessor; import org.apache.shindig.gadgets.FetchResponseUtils; import org.apache.shindig.gadgets.GadgetException; import org.apache.shindig.gadgets.http.ContentFetcherFactory; import org.apache.shindig.gadgets.http.HttpRequest; import org.apache.shindig.gadgets.http.HttpResponse; import org.apache.shindig.gadgets.oauth.OAuthArguments; import org.apache.shindig.gadgets.rewrite.ContentRewriterRegistry; import com.google.inject.Inject; import com.google.inject.Singleton; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; 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. */ @Singleton public class MakeRequestHandler extends ProxyBase { // 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 NOCACHE_PARAM = "nocache"; 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 AUTHZ_PARAM = "authz"; private final ContentFetcherFactory contentFetcherFactory; private final ContentRewriterRegistry contentRewriterRegistry; @Inject public MakeRequestHandler(ContentFetcherFactory contentFetcherFactory, ContentRewriterRegistry contentRewriterRegistry) { this.contentFetcherFactory = contentFetcherFactory; this.contentRewriterRegistry = contentRewriterRegistry; } /** * 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); // Serialize the response HttpResponse results = contentFetcherFactory.fetch(rcr); // Rewrite the response if (contentRewriterRegistry != null) { results = contentRewriterRegistry.rewriteHttpResponse(rcr, results); } // 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 */ private HttpRequest buildHttpRequest(HttpServletRequest request) throws GadgetException { String encoding = request.getCharacterEncoding(); if (encoding == null) { encoding = "UTF-8"; } Uri url = validateUrl(request.getParameter(URL_PARAM)); HttpRequest req = new HttpRequest(url) .setMethod(getParameter(request, METHOD_PARAM, "GET")) .setPostBody(getParameter(request, POST_DATA_PARAM, "").getBytes()) .setContainer(getContainer(request)); String headerData = getParameter(request, HEADERS_PARAM, ""); if (headerData.length() > 0) { 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,"); } req.addHeader(Utf8UrlCoder.decode(parts[0]), Utf8UrlCoder.decode(parts[1])); } } removeUnsafeHeaders(req); req.setIgnoreCache("1".equals(request.getParameter(NOCACHE_PARAM))); if (request.getParameter(GADGET_PARAM) != null) { req.setGadget(Uri.parse(request.getParameter(GADGET_PARAM))); } // 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 req.setRewriteMimeType(request.getParameter(REWRITE_MIME_TYPE_PARAM)); // Figure out whether authentication is required AuthType auth = AuthType.parse(getParameter(request, AUTHZ_PARAM, null)); req.setAuthType(auth); if (auth != AuthType.NONE) { req.setSecurityToken(extractAndValidateToken(request)); req.setOAuthArguments(new OAuthArguments(auth, request)); } return req; } /** * Removes unsafe headers from the header set. */ private void removeUnsafeHeaders(HttpRequest request) { // 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) { request.removeHeader(bad); } } /** * 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 { String originalUrl = request.getParameter(ProxyBase.URL_PARAM); String body = ""; if (results.getHttpStatusCode() == 200) { body = results.getResponseAsString(); if ("FEED".equals(request.getParameter(CONTENT_TYPE_PARAM))) { body = processFeed(originalUrl, request, body); } } JSONObject resp = FetchResponseUtils.getResponseAsJson(results, body); if (authToken != null) { String updatedAuthToken = authToken.getUpdatedToken(); if (updatedAuthToken != null) { resp.put("st", updatedAuthToken); } } // 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 { SecurityToken token = new AuthInfo(request).getSecurityToken(); if (token == null) { throw new GadgetException(GadgetException.Code.INVALID_SECURITY_TOKEN); } return token; } /** * 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(); } }