/*
* 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.uri;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang.StringUtils;
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 java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
/**
* Utility functions related to URI and Http servlet response management.
*
* @since 2.0.0
*/
public final class UriUtils {
public static final String CHARSET = "charset";
private static final Logger LOG = Logger.getLogger(UriUtils.class.getName());
private UriUtils() {}
/**
* Enum of disallowed response headers that should not be passed on as is to
* the user. The webserver serving out the response should be responsible
* for filling these.
*/
public enum DisallowedHeaders {
// Directives controlled by the serving infrastructure.
OUTPUT_TRANSFER_DIRECTIVES(ImmutableSet.of(
"content-length", "transfer-encoding", "content-encoding", "server",
"accept-ranges")),
CACHING_DIRECTIVES(ImmutableSet.of("vary", "expires", "date", "pragma",
"cache-control", "etag", "last-modified")),
CLIENT_STATE_DIRECTIVES(ImmutableSet.of("set-cookie", "www-authenticate")),
// Headers that the fetcher itself would like to fill. For example,
// httpclient library crashes if Content-Length header is set in the
// request being fetched.
POST_INCOMPATIBLE_DIRECTIVES(ImmutableSet.of("content-length"));
// Miscellaneous headers we should take care of, but are left for now.
// "set-cookie", "content-length", "content-encoding", "etag",
// "last-modified" ,"accept-ranges", "vary", "expires", "date",
// "pragma", "cache-control", "transfer-encoding", "www-authenticate"
private Set<String> disallowedHeaders;
DisallowedHeaders(Set<String> disallowedHeaders) {
this.disallowedHeaders = disallowedHeaders;
}
public Set<String> getDisallowedHeaders() {
return disallowedHeaders;
}
}
/**
* Returns true if the header name is valid.
* NOTE: RFC 822 section 3.1.2 describes the structure of header fields.
* According to the RFC, a header name (or field-name) must be composed of printable ASCII
* characters (i.e., characters that have values between 33. and 126. decimal, except colon).
* @param name The header name.
* @return True if the header name is valid, false otherwise.
*/
public static boolean isValidHeaderName(String name) {
char[] dst = new char[name.length()];
name.getChars(0, name.length(), dst, 0);
for (char c : dst) {
if (c < 33 || c > 126) {
return false;
}
if (c == ':') {
return false;
}
}
return true;
}
/**
* Returns true if the header value is valid.
* NOTE: RFC 822 section 3.1.2 describes the structure of header fields.
* According to the RFC, a header value (or field-body) may be composed of any ASCII characters,
* except CR or LF.
* @param val The header value.
* @return True if the header value is valid, false otherwise.
*/
public static boolean isValidHeaderValue(String val) {
char[] dst = new char[val.length()];
val.getChars(0, val.length(), dst, 0);
for (char c : dst) {
if (c == 13 || c == 10) {
// CR and LF.
return false;
}
if (c > 127) {
return false;
}
}
return true;
}
/**
* Copies the http response headers and status code to the final servlet
* response.
* @param data The http response when fetching the requested accel uri.
* @param resp The servlet response to return back to client.
* @param remapInternalServerError If true, then SC_INTERNAL_SERVER_ERROR is
* remapped to SC_BAD_GATEWAY.
* @param setHeaders If true, then setHeader method of HttpServletResponse is
* called, otherwise addHeader is called for every header.
* @param disallowedResponseHeaders Disallowed response headers to omit from the response
* returned to the user.
* @throws IOException In case the http response was not successful.
*/
public static void copyResponseHeadersAndStatusCode(
HttpResponse data, HttpResponseBuilder resp,
boolean remapInternalServerError,
boolean setHeaders,
DisallowedHeaders... disallowedResponseHeaders)
throws IOException {
// Pass original return code:
resp.setHttpStatusCode(data.getHttpStatusCode());
Set<String> allDisallowedHeaders = new HashSet<String>();
for (DisallowedHeaders h : disallowedResponseHeaders) {
allDisallowedHeaders.addAll(h.getDisallowedHeaders());
}
for (Map.Entry<String, String> entry : data.getHeaders().entries()) {
if (isValidHeaderName(entry.getKey()) && isValidHeaderValue(entry.getValue()) &&
!allDisallowedHeaders.contains(entry.getKey().toLowerCase())) {
try {
if (setHeaders) {
resp.setHeader(entry.getKey(), entry.getValue());
} else {
resp.addHeader(entry.getKey(), entry.getValue());
}
} catch (IllegalArgumentException e) {
// Skip illegal header
LOG.warning("Skipping illegal header: " + entry.getKey() + ":" + entry.getValue());
}
}
}
if (remapInternalServerError) {
// External "internal error" should be mapped to gateway error.
if (data.getHttpStatusCode() == HttpResponse.SC_INTERNAL_SERVER_ERROR) {
resp.setHttpStatusCode(HttpResponse.SC_BAD_GATEWAY);
}
}
}
/**
* Copies headers from HttpServletRequest object to HttpRequest object.
* @param origRequest Servlet request to copy headers from.
* @param req The HttpRequest object to copy headers to.
* @param disallowedRequestHeaders Disallowed request headers to omit from
* the servlet request
*/
public static void copyRequestHeaders(HttpRequest origRequest,
HttpRequest req,
DisallowedHeaders... disallowedRequestHeaders) {
Set<String> allDisallowedHeaders = new HashSet<String>();
for (DisallowedHeaders h : disallowedRequestHeaders) {
allDisallowedHeaders.addAll(h.getDisallowedHeaders());
}
for (Map.Entry<String, List<String>> inHeader : origRequest.getHeaders().entrySet()) {
String header = inHeader.getKey();
List<String> headerValues = inHeader.getValue();
if (headerValues != null && headerValues.size() > 0 &&
isValidHeaderName(header) &&
!allDisallowedHeaders.contains(header.toLowerCase())) {
// Remove existing values of this header.
req.removeHeader(header);
for (String headerVal : headerValues) {
if (isValidHeaderValue(headerVal)) {
req.addHeader(header, headerVal);
}
}
}
}
}
/**
* Copies the post data from HttpServletRequest object to HttpRequest object.
* @param origRequest Request to copy post data from.
* @param req The HttpRequest object to copy post data to.
* @throws GadgetException In case of errors.
*/
public static void copyRequestData(HttpRequest origRequest,
HttpRequest req) throws GadgetException {
req.setMethod(origRequest.getMethod());
try {
if (origRequest.getMethod().equalsIgnoreCase("post")) {
req.setPostBody(origRequest.getPostBody());
}
} catch (IOException e) {
throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
}
}
/**
* Rewrite the content type of the final http response if the request has the
* rewrite-mime-type param.
* @param req The http request.
* @param response The final http response to be returned to user.
*/
public static void maybeRewriteContentType(HttpRequest req, HttpResponseBuilder response) {
String responseType = response.getHeader("Content-Type");
String requiredType = req.getRewriteMimeType();
if (!StringUtils.isEmpty(requiredType)) {
// Use a 'Vary' style check on the response
if (requiredType.endsWith("/*") && !StringUtils.isEmpty(responseType)) {
String requiredTypePrefix = requiredType.substring(0, requiredType.length() - 1);
if (!responseType.toLowerCase().startsWith(requiredTypePrefix.toLowerCase())) {
// TODO: We are currently setting the content type to something like x/* (e.g. text/*)
// which is not a valid content type. Need to fix this.
response.setHeader("Content-Type", requiredType);
}
} else {
response.setHeader("Content-Type", requiredType);
}
}
}
/**
* Parses the value of content-type header and returns the content type header
* without the 'charset' attribute.
* @param content The content type header value.
* @return Content type header value without charset.
*/
public static String getContentTypeWithoutCharset(String content) {
String contentTypeWithoutCharset = content;
String[] parts = StringUtils.split(content, ';');
if (parts.length >= 2) {
StringBuilder contentTypeWithoutCharsetBuilder = new StringBuilder(parts.length);
contentTypeWithoutCharsetBuilder.append(parts[0]);
for (int i = 1; i < parts.length; i++) {
String parameterAndValue = parts[i].trim().toLowerCase();
String[] splits = StringUtils.split(parameterAndValue, '=');
if (splits.length > 0 && !splits[0].trim().equals(CHARSET)) {
contentTypeWithoutCharsetBuilder.append(';').append(parts[i]);
}
}
contentTypeWithoutCharset = contentTypeWithoutCharsetBuilder.toString();
}
return contentTypeWithoutCharset;
}
/**
* Called to split an authority in host and port.
* Support for IPv4 and IPv6 formats.
*
* Workaround for original code:
* String[] hostparts = StringUtils.splitPreserveAllTokens(uri.getAuthority(),':');
* with issues when IPv6 addresses like [::1] are used.
*
* @param authority String with host:port format where host can be IPv4 or IPv6 format
* @return Array with 1 or 2 elements containing host or port.
*/
public static String[] splitHostAndPort(String authority) {
String[] hostparts;
if (authority == null || authority.length() == 0) {
// This case should not happen
return null;
}
int lastColon = authority.lastIndexOf(':');
if (lastColon == -1) {
// IPv4 single without port
hostparts = new String[1];
hostparts[0] = authority;
} else {
int openBracket = authority.lastIndexOf('[');
if (openBracket == -1) {
// Two cases: a single IPv6 or IPv4 with port
String untilColon = authority.substring(0, lastColon);
boolean moreColons = untilColon.lastIndexOf(':') > -1;
if (moreColons) {
// IPv6 single without port
hostparts = new String[1];
hostparts[0] = authority;
} else {
// IPv4 with port
hostparts = new String[2];
hostparts[0] = authority.substring(0, lastColon);
hostparts[1] = authority.substring(lastColon + 1);
}
} else {
int closeBracket = authority.lastIndexOf(']');
if (closeBracket == -1) {
// This case should not happen, means authority is incorrect
return null;
}
boolean moreColons = authority.substring(closeBracket).lastIndexOf(':') > -1;
if (moreColons) {
// IPv6 with port
hostparts = new String[2];
hostparts[0] = authority.substring(0, lastColon);
hostparts[1] = authority.substring(lastColon + 1);
} else {
// IPv6 single without port
hostparts = new String[1];
hostparts[0] = authority;
}
}
}
return hostparts;
}
}