org.apache.shindig.gadgets.servlet.HttpRequestHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of shindig-gadgets Show documentation
Show all versions of shindig-gadgets Show documentation
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 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.FeedProcessor;
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.RequestPipeline;
import org.apache.shindig.gadgets.oauth.OAuthArguments;
import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
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.protocol.BaseRequestItem;
import org.apache.shindig.protocol.Operation;
import org.apache.shindig.protocol.ProtocolException;
import org.apache.shindig.protocol.Service;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.servlet.http.HttpServletResponse;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Provider;
/**
* 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.
* params : {
* href : ,
* headers : { : [, ...]},
* format : <"text", "json", "feed">
* body :
* gadget :
* authz: : ,
* sign_owner:
* sign_viewer:
* ...
* refreshInterval :
* noCache :
* sanitize :
* summarize :
* entryCount :
* }
*
* A successful response response will have the form
*
* data : {
* status :
* headers : { : [, , ...], ...}
* content : : string if 'text', JSON is 'feed' or 'json' format
* token :
* metadata : { : , ...}
* }
*
* 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 {
static final Set BAD_HEADERS = ImmutableSet.of("HOST", "ACCEPT-ENCODING");
private final RequestPipeline requestPipeline;
private final ResponseRewriterRegistry contentRewriterRegistry;
private final Provider feedProcessorProvider;
@Inject
public HttpRequestHandler(RequestPipeline requestPipeline,
@RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT)
ResponseRewriterRegistry contentRewriterRegistry,
Provider feedProcessorProvider) {
this.requestPipeline = requestPipeline;
this.contentRewriterRegistry = contentRewriterRegistry;
this.feedProcessorProvider = feedProcessorProvider;
}
/** 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);
if (httpApiRequest.body != null) {
req.setPostBody(httpApiRequest.body.getBytes());
}
// Copy over allowed headers
for (Map.Entry> 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));
}
final AuthType authType = req.getAuthType();
if (authType != AuthType.NONE) {
if (authType == AuthType.OAUTH2) {
req.setSecurityToken(requestItem.getToken());
Map authSettings = getAuthSettings(requestItem);
OAuth2Arguments oauth2Args = new OAuth2Arguments(req.getAuthType(), authSettings);
req.setOAuth2Arguments(oauth2Args);
} else {
req.setSecurityToken(requestItem.getToken());
Map authSettings = getAuthSettings(requestItem);
OAuthArguments oauthArgs = new OAuthArguments(req.getAuthType(), authSettings);
oauthArgs.setSignOwner(httpApiRequest.signOwner);
oauthArgs.setSignViewer(httpApiRequest.signViewer);
req.setOAuthArguments(oauthArgs);
}
}
// TODO: Allow the rewriter to use an externally forced mime type. This is needed
// allows proper rewriting of where x is returned with
// a content type like text/html which unfortunately happens all too often
req.setIgnoreCache(httpApiRequest.noCache);
req.setSanitizationRequested(httpApiRequest.sanitize);
// 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 (httpApiRequest.refreshInterval != null) {
req.setCacheTtl(httpApiRequest.refreshInterval);
}
HttpResponse results = requestPipeline.execute(req);
// TODO: os:HttpRequest and Preload do not use the content rewriter.
// Should we really do so here?
results = contentRewriterRegistry.rewriteHttpResponse(req, results);
HttpApiResponse httpApiResponse = new HttpApiResponse(results,
transformBody(httpApiRequest, results),
httpApiRequest);
// 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) {
throw new ProtocolException(ge.getHttpStatusCode(), ge.getMessage(), ge);
} catch (RewritingException re) {
throw new ProtocolException(re.getHttpStatusCode(),
re.getMessage(), re);
}
}
/**
* Extract all unknown keys into a map for extra auth params.
*/
private Map getAuthSettings(BaseRequestItem requestItem) {
// Keys in a request item are always Strings
@SuppressWarnings("unchecked")
Set allParameters = requestItem.getTypedRequest(Map.class).keySet();
Map authSettings = Maps.newHashMap();
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 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> 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> getHeaders() {
return headers;
}
public void setHeaders(Map> 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> 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 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.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> getHeaders() {
return headers;
}
public void setHeaders(Map> 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 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 metadata) {
this.metadata = metadata;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy