blob: db2dbff73cb8f92b213a2fff963af16ecf480c44 [file] [log] [blame]
/**
* 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.cxf.transport.http;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.HttpHeaderHelper;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.transports.http.configuration.HTTPServerPolicy;
import org.apache.cxf.version.Version;
public class Headers {
/**
* This constant is the Message(Map) key for the HttpURLConnection that
* is used to get the response.
*/
public static final String KEY_HTTP_CONNECTION = "http.connection";
/**
* Each header value is added as a separate HTTP header, example, given A header with 'a' and 'b'
* values, two A headers will be added as opposed to a single A header with the "a,b" value.
*/
public static final String ADD_HEADERS_PROPERTY = "org.apache.cxf.http.add-headers";
public static final String PROTOCOL_HEADERS_CONTENT_TYPE = Message.CONTENT_TYPE.toLowerCase();
public static final String HTTP_HEADERS_SETCOOKIE = "Set-Cookie";
public static final String HTTP_HEADERS_LINK = "Link";
public static final String EMPTY_REQUEST_PROPERTY = "org.apache.cxf.empty.request";
private static final String SET_EMPTY_REQUEST_CT_PROPERTY = "set.content.type.for.empty.request";
private static final TimeZone TIME_ZONE_GMT = TimeZone.getTimeZone("GMT");
private static final Logger LOG = LogUtils.getL7dLogger(Headers.class);
private static final List<String> SENSITIVE_HEADERS = Arrays.asList("Authorization", "Proxy-Authorization");
private static final List<Object> SENSITIVE_HEADER_MARKER = Arrays.asList("***");
private static final String ALLOW_LOGGING_SENSITIVE_HEADERS = "allow.logging.sensitive.headers";
private static final String USER_AGENT = initUserAgent();
private final Message message;
private final Map<String, List<String>> headers;
public Headers(Message message) {
this.message = message;
this.headers = getSetProtocolHeaders(message);
}
public Headers() {
this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
this.message = null;
}
public static String getUserAgent() {
return USER_AGENT;
}
private static String initUserAgent() {
String name = Version.getName();
if ("Apache CXF".equals(name)) {
name = "Apache-CXF";
}
String version = Version.getCurrentVersion();
return name + "/" + version;
}
/**
* Returns a traceable string representation of the passed-in headers map.
* The value for any keys in the map that are in the <code>SENSITIVE_HEADERS</code>
* array will be filtered out of the returned string.
* Note that this method is expensive as it will copy the map (except for the
* filtered keys), so it should be used sparingly - i.e. only when debug is
* enabled.
*/
static String toString(Map<String, List<Object>> headers, boolean logSensitiveHeaders) {
Map<String, List<Object>> filteredHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
filteredHeaders.putAll(headers);
if (!logSensitiveHeaders) {
for (String filteredKey : SENSITIVE_HEADERS) {
filteredHeaders.put(filteredKey, SENSITIVE_HEADER_MARKER);
}
}
return filteredHeaders.toString();
}
public Map<String, List<String>> headerMap() {
return headers;
}
/**
* Write cookie header from given session cookies
*
* @param sessionCookies
*/
public void writeSessionCookies(Map<String, Cookie> sessionCookies) {
List<String> cookies = null;
for (String s : headers.keySet()) {
if (HttpHeaderHelper.COOKIE.equalsIgnoreCase(s)) {
cookies = headers.remove(s);
break;
}
}
if (cookies == null) {
cookies = new ArrayList<>();
} else {
cookies = new ArrayList<>(cookies);
}
headers.put(HttpHeaderHelper.COOKIE, cookies);
for (Cookie c : sessionCookies.values()) {
cookies.add(c.requestCookieHeader());
}
}
/**
* This call places HTTP Header strings into the headers that are relevant
* to the ClientPolicy that is set on this conduit by configuration.
*
* REVISIT: A cookie is set statically from configuration?
*/
void setFromClientPolicy(HTTPClientPolicy policy) {
if (policy == null) {
return;
}
if (policy.isSetCacheControl()) {
headers.put("Cache-Control",
createMutableList(policy.getCacheControl()));
}
if (policy.isSetHost()) {
headers.put("Host",
createMutableList(policy.getHost()));
}
if (policy.isSetConnection()) {
headers.put("Connection",
createMutableList(policy.getConnection().value()));
}
if (policy.isSetAccept()) {
headers.put("Accept",
createMutableList(policy.getAccept()));
} else if (!headers.containsKey("Accept")) {
headers.put("Accept", createMutableList("*/*"));
}
if (policy.isSetAcceptEncoding()) {
headers.put("Accept-Encoding",
createMutableList(policy.getAcceptEncoding()));
}
if (policy.isSetAcceptLanguage()) {
headers.put("Accept-Language",
createMutableList(policy.getAcceptLanguage()));
}
if (policy.isSetContentType()) {
message.put(Message.CONTENT_TYPE, policy.getContentType());
}
if (policy.isSetCookie()) {
headers.put("Cookie",
createMutableList(policy.getCookie()));
}
if (policy.isSetBrowserType()) {
headers.put("User-Agent",
createMutableList(policy.getBrowserType()));
}
if (policy.isSetReferer()) {
headers.put("Referer",
createMutableList(policy.getReferer()));
}
}
void setFromServerPolicy(HTTPServerPolicy policy) {
if (policy.isSetCacheControl()) {
headers.put("Cache-Control",
createMutableList(policy.getCacheControl()));
}
if (policy.isSetContentLocation()) {
headers.put("Content-Location",
createMutableList(policy.getContentLocation()));
}
if (policy.isSetContentEncoding()) {
headers.put("Content-Encoding",
createMutableList(policy.getContentEncoding()));
}
if (policy.isSetContentType()) {
headers.put(HttpHeaderHelper.CONTENT_TYPE,
createMutableList(policy.getContentType()));
}
if (policy.isSetServerType()) {
headers.put("Server",
createMutableList(policy.getServerType()));
}
if (policy.isSetHonorKeepAlive() && !policy.isHonorKeepAlive()) {
headers.put("Connection",
createMutableList("close"));
} else if (policy.isSetKeepAliveParameters()) {
headers.put("Keep-Alive", createMutableList(policy.getKeepAliveParameters()));
}
/*
* TODO - hook up these policies
<xs:attribute name="SuppressClientSendErrors" type="xs:boolean" use="optional" default="false">
<xs:attribute name="SuppressClientReceiveErrors" type="xs:boolean" use="optional" default="false">
*/
}
public void removeAuthorizationHeaders() {
headers.remove("Authorization");
headers.remove("Proxy-Authorization");
}
public void setAuthorization(String authorization) {
headers.put("Authorization",
createMutableList(authorization));
}
public void setProxyAuthorization(String authorization) {
headers.put("Proxy-Authorization",
createMutableList(authorization));
}
/**
* While extracting the Message.PROTOCOL_HEADERS property from the Message,
* this call ensures that the Message.PROTOCOL_HEADERS property is
* set on the Message. If it is not set, an empty map is placed there, and
* then returned.
*
* @param message The outbound message
* @return The PROTOCOL_HEADERS map
*/
public static Map<String, List<String>> getSetProtocolHeaders(final Message message) {
Map<String, List<String>> headers =
CastUtils.cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
if (null == headers) {
headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
} else if (headers instanceof HashMap) {
Map<String, List<String>> headers2
= new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
headers2.putAll(headers);
headers = headers2;
}
message.put(Message.PROTOCOL_HEADERS, headers);
return headers;
}
public void readFromConnection(HttpURLConnection connection) {
Map<String, List<String>> origHeaders = connection.getHeaderFields();
headers.clear();
for (Entry<String, List<String>> entry : origHeaders.entrySet()) {
if (entry.getKey() != null) {
String key = HttpHeaderHelper.getHeaderKey(entry.getKey());
List<String> old = headers.get(key);
if (old != null) {
List<String> nl = new ArrayList<>(old.size() + entry.getValue().size());
nl.addAll(old);
nl.addAll(entry.getValue());
headers.put(key, nl);
} else {
headers.put(key, entry.getValue());
}
}
}
}
private static List<String> createMutableList(String val) {
return new ArrayList<>(Arrays.asList(val));
}
/**
* This procedure logs the PROTOCOL_HEADERS from the
* Message at the specified logging level.
*
* @param logger The Logger to log to.
* @param level The Logging Level.
* @param headersMap The Message protocol headers.
* @param logSensitiveHeaders whether to log sensitive headers
*/
static void logProtocolHeaders(Logger logger, Level level,
Map<String, List<Object>> headersMap,
boolean logSensitiveHeaders) {
if (logger.isLoggable(level)) {
for (Map.Entry<String, List<Object>> entry : headersMap.entrySet()) {
String key = entry.getKey();
boolean sensitive = !logSensitiveHeaders && SENSITIVE_HEADERS.contains(key);
List<Object> headerList = sensitive ? SENSITIVE_HEADER_MARKER : entry.getValue();
for (Object value : headerList) {
logger.log(level, key + ": "
+ (value == null ? "<null>" : value.toString()));
}
}
}
}
/**
* Set content type and protocol headers (Message.PROTOCOL_HEADERS) headers into the URL
* connection.
* Note, this does not mean they immediately get written to the output
* stream or the wire. They just just get set on the HTTP request.
*
* @param connection
* @throws IOException
*/
public void setProtocolHeadersInConnection(HttpURLConnection connection) throws IOException {
// If no Content-Type is set for empty requests then HttpUrlConnection:
// - sets a form Content-Type for empty POST
// - replaces custom Accept value with */* if HTTP proxy is used
boolean contentTypeSet = headers.containsKey(Message.CONTENT_TYPE);
if (!contentTypeSet) {
// if CT is not set then assume it has to be set by default
boolean dropContentType = false;
boolean getRequest = "GET".equals(message.get(Message.HTTP_REQUEST_METHOD));
boolean emptyRequest = getRequest || PropertyUtils.isTrue(message.get(EMPTY_REQUEST_PROPERTY));
// If it is an empty request (without a request body) then check further if CT still needs be set
if (emptyRequest) {
Object setCtForEmptyRequestProp = message.getContextualProperty(SET_EMPTY_REQUEST_CT_PROPERTY);
if (setCtForEmptyRequestProp != null) {
// If SET_EMPTY_REQUEST_CT_PROPERTY is set then do as a user prefers.
// CT will be dropped if setting CT for empty requests was explicitly disabled
dropContentType = PropertyUtils.isFalse(setCtForEmptyRequestProp);
} else if (getRequest) {
// otherwise if it is GET then just drop it
dropContentType = true;
}
}
if (!dropContentType) {
String ct = emptyRequest && !contentTypeSet ? "*/*" : determineContentType();
connection.setRequestProperty(HttpHeaderHelper.CONTENT_TYPE, ct);
}
} else {
connection.setRequestProperty(HttpHeaderHelper.CONTENT_TYPE, determineContentType());
}
transferProtocolHeadersToURLConnection(connection);
Map<String, List<Object>> theHeaders = CastUtils.cast(headers);
logProtocolHeaders(LOG, Level.FINE, theHeaders, logSensitiveHeaders());
}
public String determineContentType() {
String ct;
List<Object> ctList = CastUtils.cast(headers.get(Message.CONTENT_TYPE));
if (ctList != null && ctList.size() == 1 && ctList.get(0) != null) {
ct = ctList.get(0).toString();
} else {
ct = (String)message.get(Message.CONTENT_TYPE);
}
String enc = (String)message.get(Message.ENCODING);
if (null != ct) {
if (enc != null
&& ct.indexOf("charset=") == -1
&& !ct.toLowerCase().contains("multipart/related")) {
ct = ct + "; charset=" + enc;
}
} else if (enc != null) {
ct = "text/xml; charset=" + enc;
} else {
ct = "text/xml";
}
return ct;
}
/**
* This procedure sets the URLConnection request properties
* from the PROTOCOL_HEADERS in the message.
*/
private void transferProtocolHeadersToURLConnection(URLConnection connection) {
boolean addHeaders = MessageUtils.getContextualBoolean(message, ADD_HEADERS_PROPERTY, false);
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String header = entry.getKey();
if (HttpHeaderHelper.CONTENT_TYPE.equalsIgnoreCase(header)) {
continue;
}
List<String> headerList = entry.getValue();
if (addHeaders || HttpHeaderHelper.COOKIE.equalsIgnoreCase(header)) {
headerList.forEach(s -> connection.addRequestProperty(header, s));
} else {
connection.setRequestProperty(header, String.join(",", headerList));
}
}
// make sure we don't add more than one User-Agent header
if (connection.getRequestProperty("User-Agent") == null) {
connection.addRequestProperty("User-Agent", USER_AGENT);
}
}
/**
* Copy the request headers into the message.
*
* @param req the current servlet request
*/
protected void copyFromRequest(HttpServletRequest req) {
//TODO how to deal with the fields
for (Enumeration<String> e = req.getHeaderNames(); e.hasMoreElements();) {
String fname = e.nextElement();
String mappedName = HttpHeaderHelper.getHeaderKey(fname);
List<String> values = headers.get(mappedName);
if (values == null) {
values = new ArrayList<>();
headers.put(mappedName, values);
}
for (Enumeration<String> e2 = req.getHeaders(fname); e2.hasMoreElements();) {
String val = e2.nextElement();
if ("Accept".equals(mappedName) && !values.isEmpty()) {
//ensure we collapse Accept into first line
String firstAccept = values.get(0);
firstAccept = firstAccept + ", " + val;
values.set(0, firstAccept);
}
values.add(val);
}
}
if (!headers.containsKey(Message.CONTENT_TYPE)) {
headers.put(Message.CONTENT_TYPE, Collections.singletonList(req.getContentType()));
}
if (LOG.isLoggable(Level.FINE)) {
Map<String, List<Object>> theHeaders = CastUtils.cast(headers);
LOG.log(Level.FINE, "Request Headers: " + toString(theHeaders,
logSensitiveHeaders()));
}
}
private boolean logSensitiveHeaders() {
// Not allowed by default
return PropertyUtils.isTrue(message.getContextualProperty(ALLOW_LOGGING_SENSITIVE_HEADERS));
}
private String getContentTypeFromMessage() {
final String ct = (String)message.get(Message.CONTENT_TYPE);
final String enc = (String)message.get(Message.ENCODING);
if (null != ct
&& null != enc
&& ct.indexOf("charset=") == -1
&& !ct.toLowerCase().contains("multipart/related")) {
return ct + "; charset=" + enc;
}
return ct;
}
// Assumes that response body is not available only
// if Content-Length is available and set to 0
private boolean isResponseBodyAvailable() {
List<String> ctLen = headers.get("Content-Length");
if (ctLen == null || ctLen.size() != 1) {
return true;
}
try {
if (Integer.parseInt(ctLen.get(0)) == 0) {
return false;
}
} catch (NumberFormatException ex) {
// ignore
}
return true;
}
private boolean isSingleHeader(String header) {
return HTTP_HEADERS_SETCOOKIE.equalsIgnoreCase(header) || HTTP_HEADERS_LINK.equalsIgnoreCase(header);
}
/**
* Copy the response headers into the response.
*
* @param response the current ServletResponse
*/
protected void copyToResponse(HttpServletResponse response) {
String contentType = getContentTypeFromMessage();
if (!headers.containsKey(Message.CONTENT_TYPE) && contentType != null
&& isResponseBodyAvailable()) {
response.setContentType(contentType);
}
boolean addHeaders = MessageUtils.getContextualBoolean(message, ADD_HEADERS_PROPERTY, false);
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String header = entry.getKey();
List<?> headerList = entry.getValue();
if (addHeaders || isSingleHeader(header)) {
for (int i = 0; i < headerList.size(); i++) {
Object headerObject = headerList.get(i);
if (headerObject != null) {
response.addHeader(header, headerObjectToString(headerObject));
}
}
} else {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < headerList.size(); i++) {
Object headerObject = headerList.get(i);
if (headerObject != null) {
sb.append(headerObjectToString(headerObject));
}
if (i + 1 < headerList.size()) {
sb.append(',');
}
}
response.setHeader(header, sb.toString());
}
}
}
private String headerObjectToString(Object headerObject) {
if (headerObject.getClass() == String.class) {
// Most likely
return headerObject.toString();
}
String headerString;
if (headerObject instanceof Date) {
headerString = toHttpDate((Date)headerObject);
} else if (headerObject instanceof Locale) {
headerString = toHttpLanguage((Locale)headerObject);
} else {
headerString = headerObject.toString();
}
return headerString;
}
void removeContentType() {
headers.remove(PROTOCOL_HEADERS_CONTENT_TYPE);
}
public String getAuthorization() {
List<String> authorizationLines = headers.get("Authorization");
if (authorizationLines != null && !authorizationLines.isEmpty()) {
return authorizationLines.get(0);
}
return null;
}
public static SimpleDateFormat getHttpDateFormat() {
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
dateFormat.setTimeZone(TIME_ZONE_GMT);
return dateFormat;
}
public static String toHttpDate(Date date) {
SimpleDateFormat format = getHttpDateFormat();
return format.format(date);
}
public static String toHttpLanguage(Locale locale) {
return locale.toString().replace('_', '-');
}
}