org.infoscoop.gadgets.servlet.HttpRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.infoscoop.gadgets.servlet.HttpRequestHandler.java

Source

/*
 * 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.infoscoop.gadgets.servlet;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.JsonProperty;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.common.uri.UriBuilder;
import org.apache.shindig.gadgets.AuthType;
import org.apache.shindig.gadgets.GadgetException;
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.oauth.OAuthArguments;
import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
import org.apache.shindig.gadgets.servlet.MakeRequestHandler;
import org.apache.shindig.protocol.BaseRequestItem;
import org.apache.shindig.protocol.Operation;
import org.apache.shindig.protocol.ProtocolException;
import org.apache.shindig.protocol.Service;
import org.infoscoop.acl.SecurityController;
import org.infoscoop.web.JsonProxyServlet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

/**
 * An alternate implementation of the Http proxy service using the standard API dispatcher for REST
 * / JSON-RPC calls. The basic form of the request is as follows
 * ...
 * method : http.<HTTP method name>
 * params : {
 *    href : <endpoint to fetch content from>,
 *    headers : { <header-name> : [<header-value>, ...]},
 *    format : <"text", "json", "feed">
 *    body : <request body>
 *    gadget : <url of gadget spec for calling application>
 *    authz: : <none | oauth | signed>,
 *    sign_owner: <boolean, default true>
 *    sign_viewer: <boolean, default true>
 *    ...<additional auth arguments. See OAuthArguments>
 *    summarize : <If contentType == "FEED" summarize the results. Default false>
 *    entryCount : <If contentType == "FEED" limit results to specified no of items. Default 3>
 * }
 *
 * A successful response response will have the form
 *
 * data : {
 *    status : <HTTP status code.>
 *    headers : { <header name> : [<header val1>, <header val2>, ...], ...}
 *    content : <response body>: string if 'text', JSON is 'feed' or 'json' format
 *    token : <If security token provides a renewed value.>
 *    metadata : { <metadata entry> : <metadata value>, ...}
 * }
 *
 * It's important to note that requests which generate HTTP error responses such as 500 are returned
 * in the above format. The RPC itself succeeded in these cases. If an RPC error occurred the client
 * should introspect the error message for information as to the cause.
 *
 * TODO: send errors using "result", not plain content
 *
 * @see MakeRequestHandler
 */
@Service(name = "http")
public class HttpRequestHandler {
    private static Log log = LogFactory.getLog(HttpRequestHandler.class);
    static final Set<String> BAD_HEADERS = ImmutableSet.of("HOST", "ACCEPT-ENCODING");

    /** Execute an HTTP GET request */
    @Operation(httpMethods = { "POST", "GET" }, path = "/get")
    public HttpApiResponse get(BaseRequestItem request) {
        HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
        assertNoBody(httpReq, "GET");
        return execute("GET", httpReq, request);
    }

    /** Execute an HTTP POST request */
    @Operation(httpMethods = "POST", path = "/post")
    public HttpApiResponse post(BaseRequestItem request) {
        HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
        return execute("POST", httpReq, request);
    }

    /** Execute an HTTP PUT request */
    @Operation(httpMethods = "POST", path = "/put")
    public HttpApiResponse put(BaseRequestItem request) {
        HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
        return execute("PUT", httpReq, request);
    }

    /** Execute an HTTP DELETE request */
    @Operation(httpMethods = "POST", path = "/delete")
    public HttpApiResponse delete(BaseRequestItem request) {
        HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
        assertNoBody(httpReq, "DELETE");
        return execute("DELETE", httpReq, request);
    }

    /** Execute an HTTP HEAD request */
    @Operation(httpMethods = { "POST", "GET" }, path = "/head")
    public HttpApiResponse head(BaseRequestItem request) {
        HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
        assertNoBody(httpReq, "HEAD");
        return execute("HEAD", httpReq, request);
    }

    private void assertNoBody(HttpApiRequest httpRequest, String method) {
        if (httpRequest.body != null) {
            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
                    "Request body not supported for " + method);
        }
    }

    /**
     * Dispatch the request
     *
     * @param method HTTP method to execute
     * @param requestItem TODO
     */
    private HttpApiResponse execute(String method, HttpApiRequest httpApiRequest, BaseRequestItem requestItem) {
        if (httpApiRequest.href == null) {
            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "href parameter is missing");
        }
        // Canonicalize the path
        Uri href = normalizeUrl(httpApiRequest.href);
        try {
            HttpRequest req = new HttpRequest(href);
            req.setMethod(method);

            // Copy over allowed headers
            for (Map.Entry<String, List<String>> header : httpApiRequest.headers.entrySet()) {
                if (!BAD_HEADERS.contains(header.getKey().trim().toUpperCase())) {
                    for (String value : header.getValue()) {
                        req.addHeader(header.getKey(), value);
                    }
                }
            }

            // Extract the gadget URI from the request or the security token
            Uri gadgetUri = getGadgetUri(requestItem.getToken(), httpApiRequest);
            if (gadgetUri == null) {
                throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
                        "Gadget URI not specified in request");
            }
            req.setGadget(gadgetUri);

            // Detect the authz parsing
            if (httpApiRequest.authz != null) {
                req.setAuthType(AuthType.parse(httpApiRequest.authz));
            }

            HttpResponse results = null;
            String oauthApprovalUrl = null;
            try {
                if (httpApiRequest.body != null) {
                    req.setPostBody(httpApiRequest.body.getBytes("UTF-8"));
                }

                Map<String, String> proxyParams = new HashMap<String, String>();

                final AuthType authz = req.getAuthType();
                req.setSecurityToken(requestItem.getToken());

                if (authz == AuthType.OAUTH2) {
                    Map<String, String> authSettings = getAuthSettings(requestItem);
                    OAuth2Arguments oauth2Args = new OAuth2Arguments(authz, authSettings);

                    req.setOAuth2Arguments(oauth2Args);

                    proxyParams.put("OAUTH_SERVICE_NAME", (String) authSettings.get("OAUTH_SERVICE_NAME"));
                    proxyParams.put("hostPrefix", req.getSecurityToken().getContainer());
                    proxyParams.put("userAuthorizationURL", requestItem.getParameter("userAuthorizationURL"));
                    proxyParams.put("accessTokenURL", requestItem.getParameter("accessTokenURL"));
                    proxyParams.put("OAUTH2_SCOPE", requestItem.getParameter("OAUTH2_SCOPE"));
                } else if (authz == AuthType.OAUTH) {
                    Map<String, String> authSettings = getAuthSettings(requestItem);
                    OAuthArguments oauthArgs = new OAuthArguments(authz, authSettings);
                    oauthArgs.setSignOwner(httpApiRequest.signOwner);
                    oauthArgs.setSignViewer(httpApiRequest.signViewer);

                    req.setOAuthArguments(oauthArgs);

                    proxyParams.put("OAUTH_SERVICE_NAME", oauthArgs.getServiceName());
                    proxyParams.put("hostPrefix", req.getSecurityToken().getContainer());
                    proxyParams.put("requestTokenURL", requestItem.getParameter("requestTokenURL"));
                    proxyParams.put("requestTokenMethod", requestItem.getParameter("requestTokenMethod"));
                    proxyParams.put("userAuthorizationURL", requestItem.getParameter("userAuthorizationURL"));
                    proxyParams.put("accessTokenURL", requestItem.getParameter("accessTokenURL"));
                    proxyParams.put("accessTokenMethod", requestItem.getParameter("accessTokenMethod"));
                }
                String contentType = req.getContentType();
                String uri = req.getUri().toString();
                proxyParams.put("moduleId", req.getSecurityToken().getAppId());
                proxyParams.put("gadgetUrl", req.getGadget().toString());
                proxyParams.put("authz", authz.name());
                proxyParams.put("url", uri);
                proxyParams.put("contentType", contentType);
                proxyParams.put("httpMethod", req.getMethod());
                proxyParams.put("postData", req.getPostBodyAsString());

                String uid = SecurityController.getPrincipalByType("UIDPrincipal").getName();

                // execute request
                JSONObject resJson = JsonProxyServlet.invokeJSONProxyRequest(uid, proxyParams, req.getHeaders());
                resJson = resJson.getJSONObject(uri);

                JSONObject headersJson = new JSONObject();

                if (resJson.has("headers"))
                    headersJson = resJson.getJSONObject("headers");
                if (resJson.has("oauthApprovalUrl"))
                    oauthApprovalUrl = resJson.getString("oauthApprovalUrl");

                int rc = resJson.getInt("rc");

                results = makeResponse(rc, headersJson, resJson.getString("body"));
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new ProtocolException(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage(), e);
            }

            HttpApiResponse httpApiResponse = new HttpApiResponse(results, transformBody(httpApiRequest, results),
                    httpApiRequest);

            Map<String, String> meta = new HashMap<String, String>();
            if (oauthApprovalUrl != null)
                meta.put("oauthApprovalUrl", oauthApprovalUrl);
            httpApiResponse.setMetadata(meta);

            // Renew the security token if we can
            if (requestItem.getToken() != null) {
                String updatedAuthToken = requestItem.getToken().getUpdatedToken();
                if (updatedAuthToken != null) {
                    httpApiResponse.token = updatedAuthToken;
                }
            }
            return httpApiResponse;
        } catch (GadgetException ge) {
            log.error(ge.getMessage(), ge);
            throw new ProtocolException(ge.getHttpStatusCode(), ge.getMessage(), ge);
        }
    }

    private HttpResponse makeResponse(int rc, JSONObject headersJson, String body)
            throws IOException, JSONException {
        HttpResponseBuilder builder = new HttpResponseBuilder();

        if (headersJson != null) {
            for (@SuppressWarnings("rawtypes")
            Iterator ite = headersJson.keys(); ite.hasNext();) {
                String key = (String) ite.next();
                JSONArray values = headersJson.getJSONArray(key);
                for (int i = 0; i < values.length(); i++) {
                    builder.addHeader(key, (String) values.get(i));
                }
            }
        }

        return builder.setHttpStatusCode(rc).setResponse(body.getBytes("UTF-8")).create();
    }

    /**
     * Extract all unknown keys into a map for extra auth params.
     */
    private Map<String, String> getAuthSettings(BaseRequestItem requestItem) {
        // Keys in a request item are always Strings
        @SuppressWarnings("unchecked")
        Set<String> allParameters = requestItem.getTypedRequest(Map.class).keySet();

        //    Map<String, String> authSettings = Maps.newHashMap();
        Map<String, String> authSettings = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
        for (String paramName : allParameters) {
            if (!HttpApiRequest.KNOWN_PARAMETERS.contains(paramName)) {
                authSettings.put(paramName, requestItem.getParameter(paramName));
            }
        }

        return authSettings;
    }

    protected Uri normalizeUrl(Uri url) {
        if (url.getScheme() == null) {
            // Assume http
            url = new UriBuilder(url).setScheme("http").toUri();
        }

        if (url.getPath() == null || url.getPath().length() == 0) {
            url = new UriBuilder(url).setPath("/").toUri();
        }

        return url;
    }

    /** Format a response as JSON, including additional JSON inserted by chained content fetchers. */
    protected Object transformBody(HttpApiRequest request, HttpResponse results) throws GadgetException {
        String body = results.getResponseAsString();
        /*
        if ("feed".equalsIgnoreCase(request.format)) {
          return processFeed(request, body);
        } else
        */
        if ("json".equalsIgnoreCase(request.format)) {
            try {
                body = body.trim();
                if (body.length() > 0 && body.charAt(0) == '[') {
                    return new JSONArray(body);
                } else {
                    return new JSONObject(body);
                }
            } catch (JSONException e) {
                // TODO: include data block with invalid JSON
                throw new ProtocolException(HttpServletResponse.SC_NOT_ACCEPTABLE, "Response not valid JSON", e);
            }
        }

        return body;
    }

    /** Processes a feed (RSS or Atom) using FeedProcessor. */
    /*
    protected Object processFeed(HttpApiRequest req, String responseBody)
        throws GadgetException {
      return feedProcessorProvider.get().process(req.href.toString(), responseBody, req.summarize,
    req.entryCount);
    }
    */

    /** Extract the gadget URL from the request or the security token */
    protected Uri getGadgetUri(SecurityToken token, HttpApiRequest httpApiRequest) {
        if (token != null && token.getAppUrl() != null) {
            return Uri.parse(token.getAppUrl());
        }
        return null;
    }

    /**
     * Simple type that represents an Http request to execute on the callers behalf
     */
    public static class HttpApiRequest {
        static final Set<String> KNOWN_PARAMETERS = ImmutableSet.of("href", "headers", "body", "gadget", "authz",
                "sign_owner", "sign_viewer", "format", "refreshInterval", "noCache", "sanitize", "summarize",
                "entryCount");

        // Content to fetch / execute
        Uri href;

        Map<String, List<String>> headers = Maps.newHashMap();

        /** POST body */
        String body;

        /** Authorization type ("none", "signed", "oauth") */
        String authz = "none";

        /** Should the request be signed by owner? */
        boolean signOwner = true;

        /** Should the request be signed by viewer? */
        boolean signViewer = true;

        // The format type to coerce the response into. Supported values are
        // "text", "json", and "feed".
        String format;

        // Use Integer here to allow for null
        Integer refreshInterval;

        // Bypass http caches
        boolean noCache;

        // Use HTML/CSS sanitizer
        boolean sanitize;

        // Control feed handling
        boolean summarize;
        int entryCount = 3;

        public Uri getHref() {
            return href;
        }

        public void setHref(Uri url) {
            this.href = url;
        }

        public Map<String, List<String>> getHeaders() {
            return headers;
        }

        public void setHeaders(Map<String, List<String>> headers) {
            this.headers = headers;
        }

        public String getBody() {
            return body;
        }

        public void setBody(String body) {
            this.body = body;
        }

        public Integer getRefreshInterval() {
            return refreshInterval;
        }

        public void setRefreshInterval(Integer refreshInterval) {
            this.refreshInterval = refreshInterval;
        }

        public boolean isNoCache() {
            return noCache;
        }

        public void setNoCache(boolean noCache) {
            this.noCache = noCache;
        }

        public boolean isSanitize() {
            return sanitize;
        }

        public void setSanitize(boolean sanitize) {
            this.sanitize = sanitize;
        }

        public String getFormat() {
            return format;
        }

        public void setFormat(String format) {
            this.format = format;
        }

        public String getAuthz() {
            return authz;
        }

        public void setAuthz(String authz) {
            this.authz = authz;
        }

        public boolean isSignViewer() {
            return signViewer;
        }

        @JsonProperty("sign_viewer")
        public void setSignViewer(boolean signViewer) {
            this.signViewer = signViewer;
        }

        public boolean isSignOwner() {
            return signOwner;
        }

        @JsonProperty("sign_owner")
        public void setSignOwner(boolean signOwner) {
            this.signOwner = signOwner;
        }

        public boolean isSummarize() {
            return summarize;
        }

        public void setSummarize(boolean summarize) {
            this.summarize = summarize;
        }

        public int getEntryCount() {
            return entryCount;
        }

        public void setEntryCount(int entryCount) {
            this.entryCount = entryCount;
        }
    }

    /**
     * Response to request for Http content
     */
    public static class HttpApiResponse {
        // Http status code
        int status;

        // Returned headers
        Map<String, Collection<String>> headers;

        // Body content, either a String or a JSON-type structure
        Object content;

        // Renewed security token if available
        String token;

        // Metadata associated with the response.
        Map<String, String> metadata;

        public HttpApiResponse(int status) {
            this.status = status;
        }

        /**
         * Construct response based on HttpResponse from fetcher
         */
        public HttpApiResponse(HttpResponse response, Object content, HttpApiRequest httpApiRequest) {
            this.status = response.getHttpStatusCode();
            this.headers = new TreeMap<String, Collection<String>>(String.CASE_INSENSITIVE_ORDER);

            if (response.getHeaders().containsKey("set-cookie")) {
                this.headers.put("set-cookie", response.getHeaders("set-cookie"));
            }
            if (response.getHeaders().containsKey("location")) {
                this.headers.put("location", response.getHeaders("location"));
            }

            this.content = content;

            this.metadata = response.getMetadata();
        }

        public int getStatus() {
            return status;
        }

        public void setStatus(int status) {
            this.status = status;
        }

        public Map<String, Collection<String>> getHeaders() {
            return headers;
        }

        public void setHeaders(Map<String, Collection<String>> headers) {
            this.headers = headers;
        }

        public Object getContent() {
            return content;
        }

        public void setContent(Object content) {
            this.content = content;
        }

        public String getToken() {
            return token;
        }

        public void setToken(String token) {
            this.token = token;
        }

        public Map<String, String> getMetadata() {
            // TODO - Review this once migration of JS occurs. Currently MakeRequestHandler suppresses
            // this on output but that choice may not be the best one for compatibility.
            // Suppress metadata on output if it's empty
            if (metadata != null && metadata.isEmpty()) {
                return null;
            }
            return metadata;
        }

        public void setMetadata(Map<String, String> metadata) {
            this.metadata = metadata;
        }
    }
}