blob: 3904b106087711f124dd4455924cef24d9649271 [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.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.namespace.QName;
import org.apache.cxf.Bus;
import org.apache.cxf.common.injection.NoJSR250Annotations;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.configuration.Configurable;
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.configuration.security.AuthorizationPolicy;
import org.apache.cxf.configuration.security.CertificateConstraintsType;
import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
import org.apache.cxf.endpoint.ClientCallback;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.HttpHeaderHelper;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.helpers.LoadingByteArrayOutputStream;
import org.apache.cxf.io.AbstractThresholdOutputStream;
import org.apache.cxf.io.CacheAndWriteOutputStream;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.ExchangeImpl;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageContentsList;
import org.apache.cxf.message.MessageImpl;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.policy.PolicyDataEngine;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.transport.AbstractConduit;
import org.apache.cxf.transport.Assertor;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.MessageObserver;
import org.apache.cxf.transport.http.auth.CustomAuthSupplier;
import org.apache.cxf.transport.http.auth.DefaultBasicAuthSupplier;
import org.apache.cxf.transport.http.auth.DigestAuthSupplier;
import org.apache.cxf.transport.http.auth.HttpAuthHeader;
import org.apache.cxf.transport.http.auth.HttpAuthSupplier;
import org.apache.cxf.transport.http.auth.SpnegoAuthSupplier;
import org.apache.cxf.transport.http.policy.impl.ClientPolicyCalculator;
import org.apache.cxf.transport.https.CertConstraints;
import org.apache.cxf.transport.https.CertConstraintsInterceptor;
import org.apache.cxf.transport.https.CertConstraintsJaxBUtils;
import org.apache.cxf.transport.https.HttpsURLConnectionInfo;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.workqueue.AutomaticWorkQueue;
import org.apache.cxf.workqueue.WorkQueueManager;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
/*
* HTTP Conduit implementation.
* <p>
* This implementation is a based on the java.net.URLConnection interface and
* dependent upon installed implementations of that URLConnection,
* HttpURLConnection, and HttpsURLConnection. Currently, this implementation
* has been known to work with the Sun JDK 1.5 default implementations. The
* HttpsURLConnection is part of Sun's implementation of the JSSE.
* Presently, the source code for the Sun JSSE implementation is unavailable
* and therefore we may only lay a guess of whether its HttpsURLConnection
* implementation correctly works as far as security is concerned.
* <p>
* The Trust Decision. If a MessageTrustDecider is configured/set for the
* Conduit, it is called upon the first flush of the headers in the
* WrappedOutputStream. This reason for this approach is two-fold.
* Theoretically, in order to get connection information out of the
* URLConnection, it must be "connected". We assume that its implementation will
* only follow through up to the point at which it will be ready to send
* one byte of data down to the endpoint, but through proxies, and the
* commpletion of a TLS handshake in the case of HttpsURLConnection.
* However, if we force the connect() call right away, the default
* implementations will not allow any calls to add/setRequestProperty,
* throwing an exception that the URLConnection is already connected.
* <p>
* We need to keep the semantic that later CXF interceptors may add to the
* PROTOCOL_HEADERS in the Message. This architectual decision forces us to
* delay the connection until after that point, then pulling the trust decision.
* <p>
* The security caveat is that we don't really know when the connection is
* really established. The call to "connect" is stated to force the
* "connection," but it is a no-op if the connection was already established.
* It is entirely possible that an implementation of an URLConnection may
* indeed connect at will and start sending the headers down the connection
* during calls to add/setRequestProperty!
* <p>
* We know that the JDK 1.5 sun.com.net.www.HttpURLConnection does not send
* this information before the "connect" call, because we can look at the
* source code. However, we can only assume, not verify, that the JSSE 1.5
* HttpsURLConnection does the same, in that it is probable that the
* HttpsURLConnection shares the HttpURLConnection implementation.
* <p>
* Due to these implementations following redirects without trust checks, we
* force the URLConnection implementations not to follow redirects. If
* client side policy dictates that we follow redirects, trust decisions are
* placed before each retransmit. On a redirect, any authorization information
* dynamically acquired by a BasicAuth UserPass supplier is removed before
* being retransmitted, as it may no longer be applicable to the new url to
* which the connection is redirected.
*/
/**
* This Conduit handles the "http" and "https" transport protocols. An
* instance is governed by policies either explicitly set or by
* configuration.
*/
@NoJSR250Annotations
public abstract class HTTPConduit
extends AbstractConduit
implements Configurable, Assertor, PropertyChangeListener {
/**
* 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";
public static final String KEY_HTTP_CONNECTION_ADDRESS = "http.connection.address";
public static final String SET_HTTP_RESPONSE_MESSAGE = "org.apache.cxf.transport.http.set.response.message";
public static final String HTTP_RESPONSE_MESSAGE = "http.responseMessage";
public static final String PROCESS_FAULT_ON_HTTP_400 = "org.apache.cxf.transport.process_fault_on_http_400";
public static final String NO_IO_EXCEPTIONS = "org.apache.cxf.transport.no_io_exceptions";
/**
* The Logger for this class.
*/
protected static final Logger LOG = LogUtils.getL7dLogger(HTTPConduit.class);
private static boolean hasLoggedAsyncWarning;
/**
* This constant holds the suffix ".http-conduit" that is appended to the
* Endpoint Qname to give the configuration name of this conduit.
*/
private static final String SC_HTTP_CONDUIT_SUFFIX = ".http-conduit";
private static final String AUTO_REDIRECT_SAME_HOST_ONLY = "http.redirect.same.host.only";
private static final String AUTO_REDIRECT_ALLOW_REL_URI = "http.redirect.relative.uri";
private static final String AUTO_REDIRECT_ALLOWED_URI = "http.redirect.allowed.uri";
private static final String AUTO_REDIRECT_MAX_SAME_URI_COUNT = "http.redirect.max.same.uri.count";
private static final String HTTP_POST_METHOD = "POST";
private static final String HTTP_GET_METHOD = "GET";
private static final Set<String> KNOWN_HTTP_VERBS_WITH_NO_CONTENT =
new HashSet<>(Arrays.asList(new String[]{"GET", "HEAD", "OPTIONS", "TRACE"}));
/**
* This constant is the Message(Map) key for a list of visited URLs that
* is used in redirect loop protection.
*/
private static final String KEY_VISITED_URLS = "VisitedURLs";
/**
* This constant is the Message(Map) key for a list of URLs that
* is used in authorization loop protection.
*/
private static final String KEY_AUTH_URLS = "AuthURLs";
/**
* This field holds a reference to the CXF bus associated this conduit.
*/
protected final Bus bus;
/**
* This field is used for two reasons. First it provides the base name for
* the conduit for Spring configuration. The other is to hold default
* address information, should it not be supplied in the Message Map, by the
* Message.ENDPOINT_ADDRESS property.
*/
protected final EndpointInfo endpointInfo;
/**
* This field holds the "default" URI for this particular conduit, which
* is created on demand.
*/
protected volatile Address defaultAddress;
protected boolean fromEndpointReferenceType;
protected ProxyFactory proxyFactory;
// Configurable values
/**
* This field holds the QoS configuration settings for this conduit.
* This field is injected via spring configuration based on the conduit
* name.
*/
protected HTTPClientPolicy clientSidePolicy;
/**
* This field holds the password authorization configuration.
* This field is injected via spring configuration based on the conduit
* name.
*/
protected AuthorizationPolicy authorizationPolicy;
/**
* This field holds the password authorization configuration for the
* configured proxy. This field is injected via spring configuration based
* on the conduit name.
*/
protected ProxyAuthorizationPolicy proxyAuthorizationPolicy;
/**
* This field holds the configuration TLS configuration which
* is programmatically configured.
*/
protected TLSClientParameters tlsClientParameters;
/**
* This field contains the MessageTrustDecider.
*/
protected MessageTrustDecider trustDecider;
/**
* Implements the authentication handling when talking to a server. If it is not set
* it will be created from the authorizationPolicy.authType
*/
protected volatile HttpAuthSupplier authSupplier;
/**
* Implements the proxy authentication handling. If it is not set
* it will be created from the proxyAuthorizationPolicy.authType
*/
protected volatile HttpAuthSupplier proxyAuthSupplier;
protected Cookies cookies;
protected CertConstraints certConstraints;
private volatile boolean clientSidePolicyCalced;
/**
* Constructor
*
* @param b the associated Bus
* @param ei the endpoint info of the initiator
* @throws IOException
*/
public HTTPConduit(Bus b, EndpointInfo ei) throws IOException {
this(b,
ei,
null);
}
/**
* Constructor
*
* @param b the associated Bus.
* @param ei the endpoint info of the initiator.
* @param t the endpoint reference of the target.
* @throws IOException
*/
public HTTPConduit(Bus b,
EndpointInfo ei,
EndpointReferenceType t) throws IOException {
super(getTargetReference(ei, t, b));
bus = b;
endpointInfo = ei;
if (t != null) {
fromEndpointReferenceType = true;
}
proxyFactory = new ProxyFactory();
cookies = new Cookies();
}
/**
* updates the HTTPClientPolicy that is compatible with the assertions
* included in the service, endpoint, operation and message policy subjects
* if a PolicyDataEngine is installed
*
* wsdl extensors are superseded by policies which in
* turn are superseded by injection
*/
private void updateClientPolicy(Message m) {
if (!clientSidePolicyCalced) {
PolicyDataEngine policyEngine = bus.getExtension(PolicyDataEngine.class);
if (policyEngine != null && endpointInfo.getService() != null) {
clientSidePolicy = policyEngine.getClientEndpointPolicy(m,
endpointInfo,
this,
new ClientPolicyCalculator());
if (clientSidePolicy != null) {
clientSidePolicy.removePropertyChangeListener(this); //make sure we aren't added twice
clientSidePolicy.addPropertyChangeListener(this);
}
}
}
clientSidePolicyCalced = true;
}
private void updateClientPolicy() {
if (!clientSidePolicyCalced) {
//do no spend time on building Message and Exchange (which basically
//are ConcurrentHashMap instances) if the policy is already available
Message m = new MessageImpl();
m.setExchange(new ExchangeImpl());
m.getExchange().put(EndpointInfo.class, this.endpointInfo);
updateClientPolicy(m);
}
}
/**
* This method returns the registered Logger for this conduit.
*/
protected Logger getLogger() {
return LOG;
}
/**
* This method returns the name of the conduit, which is based on the
* endpoint name plus the SC_HTTP_CONDUIT_SUFFIX.
* @return
*/
public final String getConduitName() {
return endpointInfo.getName() + SC_HTTP_CONDUIT_SUFFIX;
}
private static void configureConduitFromEndpointInfo(HTTPConduit conduit,
EndpointInfo endpointInfo) {
if (conduit.getClient() == null) {
conduit.setClient(endpointInfo.getTraversedExtensor(
new HTTPClientPolicy(), HTTPClientPolicy.class));
}
if (conduit.getAuthorization() == null) {
conduit.setAuthorization(endpointInfo.getTraversedExtensor(
new AuthorizationPolicy(), AuthorizationPolicy.class));
}
if (conduit.getProxyAuthorization() == null) {
conduit.setProxyAuthorization(endpointInfo.getTraversedExtensor(
new ProxyAuthorizationPolicy(),
ProxyAuthorizationPolicy.class));
}
if (conduit.getTlsClientParameters() == null) {
conduit.setTlsClientParameters(endpointInfo.getTraversedExtensor(
null, TLSClientParameters.class));
}
if (conduit.getTrustDecider() == null) {
conduit.setTrustDecider(endpointInfo.getTraversedExtensor(null,
MessageTrustDecider.class));
}
if (conduit.getAuthSupplier() == null) {
conduit.setAuthSupplier(endpointInfo.getTraversedExtensor(null,
HttpAuthSupplier.class));
}
}
private void logConfig() {
if (!LOG.isLoggable(Level.FINE)) {
return;
}
if (trustDecider == null) {
LOG.log(Level.FINE,
"No Trust Decider configured for Conduit '"
+ getConduitName() + "'");
} else {
LOG.log(Level.FINE, "Message Trust Decider of class '"
+ trustDecider.getClass().getName()
+ "' with logical name of '"
+ trustDecider.getLogicalName()
+ "' has been configured for Conduit '"
+ getConduitName()
+ "'");
}
if (authSupplier == null) {
LOG.log(Level.FINE,
"No Auth Supplier configured for Conduit '"
+ getConduitName() + "'");
} else {
LOG.log(Level.FINE, "HttpAuthSupplier of class '"
+ authSupplier.getClass().getName()
+ "' has been configured for Conduit '"
+ getConduitName()
+ "'");
}
if (this.tlsClientParameters != null) {
LOG.log(Level.FINE, "Conduit '" + getConduitName()
+ "' has been configured for TLS "
+ "keyManagers " + Arrays.toString(tlsClientParameters.getKeyManagers())
+ "trustManagers " + Arrays.toString(tlsClientParameters.getTrustManagers())
+ "secureRandom " + tlsClientParameters.getSecureRandom()
+ "Disable Common Name (CN) Check: " + tlsClientParameters.isDisableCNCheck());
} else {
LOG.log(Level.FINE, "Conduit '" + getConduitName()
+ "' has been configured for plain http.");
}
}
/**
* This call gets called by the HTTPTransportFactory after it
* causes an injection of the Spring configuration properties
* of this Conduit.
*/
public void finalizeConfig() {
// See if not set by configuration, if there are defaults
// in order from the Endpoint, Service, or Bus.
configureConduitFromEndpointInfo(this, endpointInfo);
logConfig();
if (getClient().getDecoupledEndpoint() != null) {
this.endpointInfo.setProperty("org.apache.cxf.ws.addressing.replyto",
getClient().getDecoupledEndpoint());
}
}
/**
* Allow access to the cookies that the conduit is maintaining
* @return the sessionCookies map
*/
public Map<String, Cookie> getCookies() {
return cookies.getSessionCookies();
}
protected abstract void setupConnection(Message message, Address address, HTTPClientPolicy csPolicy)
throws IOException;
/**
* Prepare to send an outbound HTTP message over this http conduit to a
* particular endpoint.
* <P>
* If the Message.PATH_INFO property is set it gets appended
* to the Conduit's endpoint URL. If the Message.QUERY_STRING
* property is set, it gets appended to the resultant URL following
* a "?".
* <P>
* If the Message.HTTP_REQUEST_METHOD property is NOT set, the
* Http request method defaults to "POST".
* <P>
* If the Message.PROTOCOL_HEADERS is not set on the message, it is
* initialized to an empty map.
* <P>
* This call creates the OutputStream for the content of the message.
* It also assigns the created Http(s)URLConnection to the Message
* Map.
*
* @param message The message to be sent.
*/
public void prepare(Message message) throws IOException {
// This call can possibly change the conduit endpoint address and
// protocol from the default set in EndpointInfo that is associated
// with the Conduit.
Address currentAddress;
try {
currentAddress = setupAddress(message);
} catch (URISyntaxException e) {
throw new IOException(e);
}
// The need to cache the request is off by default
boolean needToCacheRequest = false;
HTTPClientPolicy csPolicy = getClient(message);
setupConnection(message, currentAddress, csPolicy);
// If the HTTP_REQUEST_METHOD is not set, the default is "POST".
String httpRequestMethod =
(String)message.get(Message.HTTP_REQUEST_METHOD);
if (httpRequestMethod == null) {
httpRequestMethod = "POST";
message.put(Message.HTTP_REQUEST_METHOD, "POST");
}
boolean isChunking = false;
int chunkThreshold = 0;
final AuthorizationPolicy effectiveAuthPolicy = getEffectiveAuthPolicy(message);
if (this.authSupplier == null) {
this.authSupplier = createAuthSupplier(effectiveAuthPolicy);
}
if (this.proxyAuthSupplier == null) {
this.proxyAuthSupplier = createAuthSupplier(proxyAuthorizationPolicy);
}
if (this.authSupplier.requiresRequestCaching()) {
needToCacheRequest = true;
isChunking = false;
LOG.log(Level.FINE,
"Auth Supplier, but no Preemptive User Pass or Digest auth (nonce may be stale)"
+ " We must cache request.");
}
if (csPolicy.isAutoRedirect()) {
needToCacheRequest = true;
LOG.log(Level.FINE, "AutoRedirect is turned on.");
}
if (csPolicy.getMaxRetransmits() > 0) {
needToCacheRequest = true;
LOG.log(Level.FINE, "MaxRetransmits is set > 0.");
}
// DELETE does not work and empty PUTs cause misleading exceptions
// if chunking is enabled
// TODO : ensure chunking can be enabled for non-empty PUTs - if requested
if (csPolicy.isAllowChunking()
&& isChunkingSupported(message, httpRequestMethod)) {
//TODO: The chunking mode be configured or at least some
// documented client constant.
//use -1 and allow the URL connection to pick a default value
isChunking = true;
chunkThreshold = csPolicy.getChunkingThreshold();
}
cookies.writeToMessageHeaders(message);
// The trust decision is relegated to after the "flushing" of the
// request headers.
if (certConstraints != null) {
message.put(CertConstraints.class.getName(), certConstraints);
message.getInterceptorChain().add(CertConstraintsInterceptor.INSTANCE);
}
setHeadersByAuthorizationPolicy(message, currentAddress.getURI());
new Headers(message).setFromClientPolicy(getClient(message));
// set the OutputStream on the ProxyOutputStream
ProxyOutputStream pos = message.getContent(ProxyOutputStream.class);
if (pos != null && message.getContent(OutputStream.class) != null) {
pos.setWrappedOutputStream(createOutputStream(message,
needToCacheRequest,
isChunking,
chunkThreshold));
} else {
message.setContent(OutputStream.class,
createOutputStream(message,
needToCacheRequest,
isChunking,
chunkThreshold));
}
// We are now "ready" to "send" the message.
}
protected boolean isChunkingSupported(Message message, String httpMethod) {
if (HTTP_POST_METHOD.equals(httpMethod)) {
return true;
} else if (!HTTP_GET_METHOD.equals(httpMethod)) {
MessageContentsList objs = MessageContentsList.getContentsList(message);
if (objs != null && !objs.isEmpty()) {
Object obj = objs.get(0);
return obj.getClass() != String.class
|| (obj.getClass() == String.class && ((String)obj).length() > 0);
}
}
return false;
}
protected abstract OutputStream createOutputStream(Message message,
boolean needToCacheRequest,
boolean isChunking,
int chunkThreshold) throws IOException;
private HttpAuthSupplier createAuthSupplier(AuthorizationPolicy authzPolicy) {
String authType = authzPolicy.getAuthorizationType();
if (HttpAuthHeader.AUTH_TYPE_NEGOTIATE.equals(authType)) {
return new SpnegoAuthSupplier();
} else if (HttpAuthHeader.AUTH_TYPE_DIGEST.equals(authType)) {
return new DigestAuthSupplier();
} else if (authType != null && !HttpAuthHeader.AUTH_TYPE_BASIC.equals(authType)
&& authzPolicy.getAuthorization() != null) {
return new CustomAuthSupplier();
} else {
return new DefaultBasicAuthSupplier();
}
}
protected static int determineReceiveTimeout(Message message,
HTTPClientPolicy csPolicy) {
long rtimeout = csPolicy.getReceiveTimeout();
if (message.get(Message.RECEIVE_TIMEOUT) != null) {
Object obj = message.get(Message.RECEIVE_TIMEOUT);
try {
rtimeout = Long.parseLong(obj.toString());
} catch (NumberFormatException e) {
LOG.log(Level.WARNING, "INVALID_TIMEOUT_FORMAT", new Object[] {
Message.RECEIVE_TIMEOUT, obj.toString()
});
}
}
if (rtimeout > Integer.MAX_VALUE) {
rtimeout = Integer.MAX_VALUE;
}
return (int)rtimeout;
}
protected static int determineConnectionTimeout(Message message,
HTTPClientPolicy csPolicy) {
long ctimeout = csPolicy.getConnectionTimeout();
if (message.get(Message.CONNECTION_TIMEOUT) != null) {
Object obj = message.get(Message.CONNECTION_TIMEOUT);
try {
ctimeout = Long.parseLong(obj.toString());
} catch (NumberFormatException e) {
LOG.log(Level.WARNING, "INVALID_TIMEOUT_FORMAT", new Object[] {
Message.CONNECTION_TIMEOUT, obj.toString()
});
}
}
if (ctimeout > Integer.MAX_VALUE) {
ctimeout = Integer.MAX_VALUE;
}
return (int)ctimeout;
}
public void close(Message msg) throws IOException {
InputStream in = msg.getContent(InputStream.class);
try {
if (in != null) {
int count = 0;
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1
&& count < 25) {
//don't do anything, we just need to pull off the unread data (like
//closing tags that we didn't need to read
//however, limit it so we don't read off gigabytes of data we won't use.
++count;
}
}
} finally {
super.close(msg);
}
}
/**
* This function sets up a URL based on ENDPOINT_ADDRESS, PATH_INFO,
* and QUERY_STRING properties in the Message. The QUERY_STRING gets
* added with a "?" after the PATH_INFO. If the ENDPOINT_ADDRESS is not
* set on the Message, the endpoint address is taken from the
* "defaultEndpointURL".
* <p>
* The PATH_INFO is only added to the endpoint address string should
* the PATH_INFO not equal the end of the endpoint address string.
*
* @param message The message holds the addressing information.
*
* @return The full URL specifying the HTTP request to the endpoint.
*
* @throws MalformedURLException
* @throws URISyntaxException
*/
private Address setupAddress(Message message) throws URISyntaxException {
String result = (String)message.get(Message.ENDPOINT_ADDRESS);
String pathInfo = (String)message.get(Message.PATH_INFO);
String queryString = (String)message.get(Message.QUERY_STRING);
setAndGetDefaultAddress();
if (result == null) {
if (pathInfo == null && queryString == null) {
if (defaultAddress != null) {
message.put(Message.ENDPOINT_ADDRESS, defaultAddress.getString());
}
return defaultAddress;
}
if (defaultAddress != null) {
result = defaultAddress.getString();
message.put(Message.ENDPOINT_ADDRESS, result);
}
}
// REVISIT: is this really correct?
if (null != pathInfo && !result.endsWith(pathInfo)) {
result = result + pathInfo;
}
if (queryString != null) {
result = result + "?" + queryString;
}
if (defaultAddress == null) {
return setAndGetDefaultAddress(result);
}
return result.equals(defaultAddress.getString()) ? defaultAddress : new Address(result);
}
/**
* Close the conduit
*/
public void close() {
if (clientSidePolicy != null) {
clientSidePolicy.removePropertyChangeListener(this);
}
}
/**
* @return the default target address
*/
public String getAddress() {
if (defaultAddress != null) {
return defaultAddress.getString();
} else if (fromEndpointReferenceType) {
return getTarget().getAddress().getValue();
}
return endpointInfo.getAddress();
}
/**
* @return the default target URL
*/
protected URI getURI() throws URISyntaxException {
return setAndGetDefaultAddress().getURI();
}
private Address setAndGetDefaultAddress() throws URISyntaxException {
if (defaultAddress == null) {
synchronized (this) {
if (defaultAddress == null) {
if (fromEndpointReferenceType && getTarget().getAddress().getValue() != null) {
defaultAddress = new Address(this.getTarget().getAddress().getValue());
} else if (endpointInfo.getAddress() != null) {
defaultAddress = new Address(endpointInfo.getAddress());
}
}
}
}
return defaultAddress;
}
private Address setAndGetDefaultAddress(String curAddr) throws URISyntaxException {
if (defaultAddress == null) {
synchronized (this) {
if (defaultAddress == null) {
if (curAddr != null) {
defaultAddress = new Address(curAddr);
} else {
throw new URISyntaxException("<null>",
"Invalid address. Endpoint address cannot be null.", 0);
}
}
}
}
return defaultAddress;
}
/**
* This call places HTTP Header strings into the headers that are relevant
* to the Authorization policies that are set on this conduit by
* configuration.
* <p>
* An AuthorizationPolicy may also be set on the message. If so, those
* policies are merged. A user name or password set on the messsage
* overrides settings in the AuthorizationPolicy is retrieved from the
* configuration.
* <p>
* The precedence is as follows:
* 1. AuthorizationPolicy that is set on the Message, if exists.
* 2. Authorization from AuthSupplier, if exists.
* 3. AuthorizationPolicy set/configured for conduit.
*
* REVISIT: Since the AuthorizationPolicy is set on the message by class, then
* how does one override the ProxyAuthorizationPolicy which is the same
* type?
*
* @param message
* @param currentURI
*/
protected void setHeadersByAuthorizationPolicy(
Message message,
URI currentURI
) {
Headers headers = new Headers(message);
AuthorizationPolicy effectiveAuthPolicy = getEffectiveAuthPolicy(message);
String authString = authSupplier.getAuthorization(effectiveAuthPolicy, currentURI, message, null);
if (authString != null) {
headers.setAuthorization(authString);
}
String proxyAuthString = proxyAuthSupplier.getAuthorization(proxyAuthorizationPolicy,
currentURI, message, null);
if (proxyAuthString != null) {
headers.setProxyAuthorization(proxyAuthString);
}
}
/**
* This is part of the Configurable interface which retrieves the
* configuration from spring injection.
*/
// REVISIT:What happens when the endpoint/bean name is null?
public String getBeanName() {
if (endpointInfo.getName() != null) {
return endpointInfo.getName().toString() + ".http-conduit";
}
return null;
}
/**
* Determines effective auth policy from message, conduit and empty default
* with priority from first to last
*
* @param message
* @return effective AthorizationPolicy
*/
public AuthorizationPolicy getEffectiveAuthPolicy(Message message) {
AuthorizationPolicy authPolicy = getAuthorization();
AuthorizationPolicy newPolicy = message.get(AuthorizationPolicy.class);
AuthorizationPolicy effectivePolicy = newPolicy;
if (effectivePolicy == null) {
effectivePolicy = authPolicy;
}
if (effectivePolicy == null) {
effectivePolicy = new AuthorizationPolicy();
}
return effectivePolicy;
}
/**
* This method gets the Authorization Policy that was configured or
* explicitly set for this HTTPConduit.
*/
public AuthorizationPolicy getAuthorization() {
return authorizationPolicy;
}
/**
* This method is used to set the Authorization Policy for this conduit.
* Using this method will override any Authorization Policy set in
* configuration.
*/
public void setAuthorization(AuthorizationPolicy authorization) {
this.authorizationPolicy = authorization;
}
public HTTPClientPolicy getClient(Message message) {
ClientPolicyCalculator cpc = new ClientPolicyCalculator();
HTTPClientPolicy pol = message.get(HTTPClientPolicy.class);
updateClientPolicy(message);
if (pol != null) {
pol = cpc.intersect(pol, clientSidePolicy);
} else {
pol = clientSidePolicy;
}
PolicyDataEngine policyDataEngine = bus.getExtension(PolicyDataEngine.class);
if (policyDataEngine == null) {
return pol;
}
return policyDataEngine.getPolicy(message, pol, cpc);
}
/**
* This method retrieves the Client Side Policy set/configured for this
* HTTPConduit.
*/
public HTTPClientPolicy getClient() {
updateClientPolicy();
return clientSidePolicy;
}
/**
* This method sets the Client Side Policy for this HTTPConduit. Using this
* method will override any HTTPClientPolicy set in configuration.
*/
public void setClient(HTTPClientPolicy client) {
if (this.clientSidePolicy != null) {
this.clientSidePolicy.removePropertyChangeListener(this);
}
this.clientSidePolicyCalced = true;
this.clientSidePolicy = client;
clientSidePolicy.removePropertyChangeListener(this); //make sure we aren't added twice
clientSidePolicy.addPropertyChangeListener(this);
endpointInfo.setProperty("org.apache.cxf.ws.addressing.replyto", client.getDecoupledEndpoint());
}
/**
* This method retrieves the Proxy Authorization Policy for a proxy that is
* set/configured for this HTTPConduit.
*/
public ProxyAuthorizationPolicy getProxyAuthorization() {
return proxyAuthorizationPolicy;
}
/**
* This method sets the Proxy Authorization Policy for a specified proxy.
* Using this method overrides any Authorization Policy for the proxy
* that is set in the configuration.
*/
public void setProxyAuthorization(
ProxyAuthorizationPolicy proxyAuthorization
) {
this.proxyAuthorizationPolicy = proxyAuthorization;
}
/**
* This method returns the TLS Client Parameters that is set/configured
* for this HTTPConduit.
*/
public TLSClientParameters getTlsClientParameters() {
return tlsClientParameters;
}
/**
* This method sets the TLS Client Parameters for this HTTPConduit.
* Using this method overrides any TLS Client Parameters that is configured
* for this HTTPConduit.
*/
public void setTlsClientParameters(TLSClientParameters params) {
this.tlsClientParameters = params;
if (this.tlsClientParameters != null) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Conduit '" + getConduitName()
+ "' has been (re) configured for TLS "
+ "keyManagers " + Arrays.toString(tlsClientParameters.getKeyManagers())
+ "trustManagers " + Arrays.toString(tlsClientParameters.getTrustManagers())
+ "secureRandom " + tlsClientParameters.getSecureRandom());
}
CertificateConstraintsType constraints = params.getCertConstraints();
if (constraints != null) {
certConstraints = CertConstraintsJaxBUtils.createCertConstraints(constraints);
}
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Conduit '" + getConduitName()
+ "' has been (re)configured for plain http.");
}
}
}
/**
* This method gets the Trust Decider that was set/configured for this
* HTTPConduit.
* @return The Message Trust Decider or null.
*/
public MessageTrustDecider getTrustDecider() {
return this.trustDecider;
}
/**
* This method sets the Trust Decider for this HTTP Conduit.
* Using this method overrides any trust decider configured for this
* HTTPConduit.
*/
public void setTrustDecider(MessageTrustDecider decider) {
this.trustDecider = decider;
}
/**
* This method gets the Auth Supplier that was set/configured for this
* HTTPConduit.
* @return The Auth Supplier or null.
*/
public HttpAuthSupplier getAuthSupplier() {
return this.authSupplier;
}
public void setAuthSupplier(HttpAuthSupplier supplier) {
this.authSupplier = supplier;
}
public HttpAuthSupplier getProxyAuthSupplier() {
return proxyAuthSupplier;
}
public void setProxyAuthSupplier(HttpAuthSupplier proxyAuthSupplier) {
this.proxyAuthSupplier = proxyAuthSupplier;
}
/**
* This method extracts the value of the "Location" Http
* Response header.
*
* @param headers The Http response headers.
* @return The value of the "Location" header, null if non-existent.
* @throws MalformedURLException
*/
protected String extractLocation(Map<String, List<String>> headers) throws MalformedURLException {
for (Map.Entry<String, List<String>> head : headers.entrySet()) {
if ("Location".equalsIgnoreCase(head.getKey())) {
List<String> locs = head.getValue();
if (locs != null && !locs.isEmpty()) {
String location = locs.get(0);
if (location != null) {
return location;
}
return null;
}
}
}
return null;
}
/**
* Used to set appropriate message properties, exchange etc.
* as required for an incoming decoupled response (as opposed
* what's normally set by the Destination for an incoming
* request).
*/
protected class InterposedMessageObserver implements MessageObserver {
/**
* Called for an incoming message.
*
* @param inMessage
*/
public void onMessage(Message inMessage) {
// disposable exchange, swapped with real Exchange on correlation
inMessage.setExchange(new ExchangeImpl());
inMessage.getExchange().put(Bus.class, bus);
inMessage.put(Message.DECOUPLED_CHANNEL_MESSAGE, Boolean.TRUE);
// REVISIT: how to get response headers?
//inMessage.put(Message.PROTOCOL_HEADERS, req.getXXX());
Headers.getSetProtocolHeaders(inMessage);
inMessage.put(Message.RESPONSE_CODE, HttpURLConnection.HTTP_OK);
// remove server-specific properties
inMessage.remove(AbstractHTTPDestination.HTTP_REQUEST);
inMessage.remove(AbstractHTTPDestination.HTTP_RESPONSE);
inMessage.remove(Message.ASYNC_POST_RESPONSE_DISPATCH);
//cache this inputstream since it's defer to use in case of async
try {
InputStream in = inMessage.getContent(InputStream.class);
if (in != null) {
CachedOutputStream cos = new CachedOutputStream();
IOUtils.copy(in, cos);
inMessage.setContent(InputStream.class, cos.getInputStream());
}
incomingObserver.onMessage(inMessage);
} catch (IOException e) {
logStackTrace(e);
}
}
}
protected void logStackTrace(Throwable ex) {
StringWriter sw = new StringWriter();
ex.printStackTrace(new PrintWriter(sw));
LOG.warning(sw.toString());
}
public void assertMessage(Message message) {
PolicyDataEngine policyDataEngine = bus.getExtension(PolicyDataEngine.class);
policyDataEngine.assertMessage(message, getClient(), new ClientPolicyCalculator());
}
public boolean canAssert(QName type) {
return type.equals(new QName("http://cxf.apache.org/transports/http/configuration", "client"));
}
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getSource() == clientSidePolicy
&& "decoupledEndpoint".equals(evt.getPropertyName())) {
this.endpointInfo.setProperty("org.apache.cxf.ws.addressing.replyto",
evt.getNewValue());
}
}
/**
* Wrapper output stream responsible for flushing headers and handling
* the incoming HTTP-level response (not necessarily the MEP response).
*/
protected abstract class WrappedOutputStream extends AbstractThresholdOutputStream {
/**
* This boolean is true if the request must be cached.
*/
protected boolean cachingForRetransmission;
/**
* If we are going to be chunking, we won't flush till close which causes
* new chunks, small network packets, etc..
*/
protected final boolean chunking;
/**
* This field contains the output stream with which we cache
* the request. It maybe null if we are not caching.
*/
protected CacheAndWriteOutputStream cachedStream;
protected Message outMessage;
protected String conduitName;
protected URI url;
protected WrappedOutputStream(
Message outMessage,
boolean possibleRetransmit,
boolean isChunking,
int chunkThreshold,
String conduitName,
URI url
) {
super(chunkThreshold);
this.outMessage = outMessage;
this.cachingForRetransmission = possibleRetransmit;
this.chunking = isChunking;
this.conduitName = conduitName;
this.url = url;
}
// This construction makes extending the HTTPConduit more easier
protected WrappedOutputStream(WrappedOutputStream wos) {
super(wos.threshold);
this.outMessage = wos.outMessage;
this.cachingForRetransmission = wos.cachingForRetransmission;
this.chunking = wos.chunking;
this.conduitName = wos.conduitName;
this.url = wos.url;
}
@Override
public void thresholdNotReached() {
if (chunking) {
setFixedLengthStreamingMode(buffer.size());
}
}
// methods used for the outgoing side
protected abstract void setupWrappedStream() throws IOException;
protected abstract HttpsURLConnectionInfo getHttpsURLConnectionInfo() throws IOException;
protected abstract void setProtocolHeaders() throws IOException;
protected abstract void setFixedLengthStreamingMode(int i);
// methods used for the incoming side
protected abstract int getResponseCode() throws IOException;
protected abstract String getResponseMessage() throws IOException;
protected abstract void updateResponseHeaders(Message inMessage) throws IOException;
protected abstract void handleResponseAsync() throws IOException;
protected abstract void closeInputStream() throws IOException;
protected abstract boolean usingProxy();
protected abstract InputStream getInputStream() throws IOException;
protected abstract InputStream getPartialResponse() throws IOException;
//methods to support retransmission for auth or redirects
protected abstract void setupNewConnection(String newURL) throws IOException;
protected abstract void retransmitStream() throws IOException;
protected abstract void updateCookiesBeforeRetransmit() throws IOException;
protected void handleNoOutput() throws IOException {
//For GET and DELETE and such, this will be called
//For some implementations, this notice may be required to
//actually execute the request
}
protected void handleResponseOnWorkqueue(boolean allowCurrentThread, boolean forceWQ) throws IOException {
Runnable runnable = new Runnable() {
public void run() {
try {
handleResponseInternal();
} catch (Throwable e) {
((PhaseInterceptorChain)outMessage.getInterceptorChain()).abort();
outMessage.setContent(Exception.class, e);
((PhaseInterceptorChain)outMessage.getInterceptorChain()).unwind(outMessage);
MessageObserver mo = outMessage.getInterceptorChain().getFaultObserver();
if (mo == null) {
mo = outMessage.getExchange().get(MessageObserver.class);
}
mo.onMessage(outMessage);
}
}
};
HTTPClientPolicy policy = getClient(outMessage);
boolean exceptionSet = outMessage.getContent(Exception.class) != null;
if (!exceptionSet) {
try {
Executor ex = outMessage.getExchange().get(Executor.class);
if (forceWQ && ex != null) {
final Executor ex2 = ex;
final Runnable origRunnable = runnable;
runnable = new Runnable() {
public void run() {
outMessage.getExchange().put(Executor.class.getName()
+ ".USING_SPECIFIED", Boolean.TRUE);
ex2.execute(origRunnable);
}
};
}
if (ex == null || forceWQ) {
WorkQueueManager mgr = outMessage.getExchange().getBus()
.getExtension(WorkQueueManager.class);
AutomaticWorkQueue qu = mgr.getNamedWorkQueue("http-conduit");
if (qu == null) {
qu = mgr.getAutomaticWorkQueue();
}
long timeout = 1000;
if (policy != null && policy.isSetAsyncExecuteTimeout()) {
timeout = policy.getAsyncExecuteTimeout();
}
if (timeout > 0) {
qu.execute(runnable, timeout);
} else {
qu.execute(runnable);
}
} else {
outMessage.getExchange().put(Executor.class.getName()
+ ".USING_SPECIFIED", Boolean.TRUE);
ex.execute(runnable);
}
} catch (RejectedExecutionException rex) {
if (!allowCurrentThread
|| (policy != null
&& policy.isSetAsyncExecuteTimeoutRejection()
&& policy.isAsyncExecuteTimeoutRejection())) {
throw rex;
}
if (!hasLoggedAsyncWarning) {
LOG.warning("EXECUTOR_FULL_WARNING");
hasLoggedAsyncWarning = true;
}
LOG.fine("EXECUTOR_FULL");
handleResponseInternal();
}
}
}
protected void retransmit(String newURL) throws IOException {
setupNewConnection(newURL);
if (cachedStream != null && cachedStream.size() < Integer.MAX_VALUE) {
setFixedLengthStreamingMode((int)cachedStream.size());
}
setProtocolHeaders();
//
// This point is where the trust decision is made because the
// Sun implementation of URLConnection will not let us
// set/addRequestProperty after a connect() call, and
// makeTrustDecision needs to make a connect() call to
// make sure the proper information is available.
//
makeTrustDecision();
// If this is a GET method we must not touch the output
// stream as this automagically turns the request into a POST.
if ("GET".equals(getMethod()) || cachedStream == null) {
handleNoOutput();
return;
}
// Trust is okay, write the cached request
retransmitStream();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Conduit \""
+ getConduitName()
+ "\" Retransmit message to: "
+ newURL
+ ": "
+ new String(cachedStream.getBytes()));
}
}
/**
* Perform any actions required on stream flush (freeze headers,
* reset output stream ... etc.)
*/
@Override
protected void onFirstWrite() throws IOException {
try {
handleHeadersTrustCaching();
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().contains("HTTPS hostname wrong:")) {
throw new IOException("The https URL hostname does not match the "
+ "Common Name (CN) on the server certificate in the client's truststore. "
+ "Make sure server certificate is correct, or to disable this check "
+ "(NOT recommended for production) set the CXF client TLS "
+ "configuration property \"disableCNCheck\" to true.");
}
throw e;
}
}
protected String getMethod() {
return (String)outMessage.get(Message.HTTP_REQUEST_METHOD);
}
protected void handleHeadersTrustCaching() throws IOException {
// Need to set the headers before the trust decision
// because they are set before the connect().
setProtocolHeaders();
//
// This point is where the trust decision is made because the
// Sun implementation of URLConnection will not let us
// set/addRequestProperty after a connect() call, and
// makeTrustDecision needs to make a connect() call to
// make sure the proper information is available.
//
makeTrustDecision();
// Trust is okay, set up for writing the request.
String method = getMethod();
if (KNOWN_HTTP_VERBS_WITH_NO_CONTENT.contains(method)
|| PropertyUtils.isTrue(outMessage.get(Headers.EMPTY_REQUEST_PROPERTY))) {
handleNoOutput();
return;
}
setupWrappedStream();
}
/**
* Perform any actions required on stream closure (handle response etc.)
*/
public void close() throws IOException {
try {
if (buffer != null && buffer.size() > 0) {
thresholdNotReached();
LoadingByteArrayOutputStream tmp = buffer;
buffer = null;
super.write(tmp.getRawBytes(), 0, tmp.size());
}
boolean exceptionSet = outMessage.getContent(Exception.class) != null;
if (!written && !exceptionSet) {
handleHeadersTrustCaching();
}
if (!cachingForRetransmission) {
super.close();
} else if (cachedStream != null) {
super.flush();
cachedStream.getOut().close();
cachedStream.closeFlowthroughStream();
}
try {
handleResponse();
} finally {
if (cachingForRetransmission && cachedStream != null) {
cachedStream.close();
}
}
} catch (HttpRetryException e) {
handleHttpRetryException(e);
} catch (IOException e) {
String origMessage = e.getMessage();
if (origMessage != null && origMessage.contains(url.toString())) {
throw e;
}
throw mapException(e.getClass().getSimpleName()
+ " invoking " + url + ": "
+ e.getMessage(), e,
IOException.class);
} catch (RuntimeException e) {
throw mapException(e.getClass().getSimpleName()
+ " invoking " + url + ": "
+ e.getMessage(), e,
RuntimeException.class);
}
}
private <T extends Exception> T mapException(String msg,
T ex, Class<T> cls) {
T ex2;
try {
ex2 = cls.cast(ex.getClass().getConstructor(String.class).newInstance(msg));
ex2.initCause(ex);
} catch (Throwable e) {
ex2 = ex;
}
return ex2;
}
/**
* This procedure handles all retransmits, if any.
*
* @throws IOException
*/
protected void handleRetransmits() throws IOException {
// If we have a cachedStream, we are caching the request.
if (cachedStream != null
|| getClient().isAutoRedirect() && KNOWN_HTTP_VERBS_WITH_NO_CONTENT.contains(getMethod())
|| authSupplier != null && authSupplier.requiresRequestCaching()) {
if (LOG.isLoggable(Level.FINE) && cachedStream != null) {
StringBuilder b = new StringBuilder(4096);
b.append("Conduit \"").append(getConduitName())
.append("\" Transmit cached message to: ")
.append(url)
.append(": ");
cachedStream.writeCacheTo(b, 16L * 1024L);
LOG.fine(b.toString());
}
int maxRetransmits = getMaxRetransmits();
updateCookiesBeforeRetransmit();
int nretransmits = 0;
while ((maxRetransmits < 0 || nretransmits < maxRetransmits) && processRetransmit()) {
nretransmits++;
}
}
}
/**
* This function processes any retransmits at the direction of redirections
* or "unauthorized" responses.
*
* @return true if there was a retransmit
* @throws IOException
*/
protected boolean processRetransmit() throws IOException {
int responseCode = getResponseCode();
if ((outMessage != null) && (outMessage.getExchange() != null)) {
outMessage.getExchange().put(Message.RESPONSE_CODE, responseCode);
}
// Process Redirects first.
switch(responseCode) {
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_SEE_OTHER:
case 307:
case 308:
return redirectRetransmit();
case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_PROXY_AUTH:
return authorizationRetransmit();
default:
break;
}
return false;
}
protected boolean redirectRetransmit() throws IOException {
// If we are not redirecting by policy, then we don't.
if (!getClient(outMessage).isAutoRedirect()) {
return false;
}
Message m = new MessageImpl();
updateResponseHeaders(m);
String newURL = extractLocation(Headers.getSetProtocolHeaders(m));
String urlString = url.toString();
try {
newURL = convertToAbsoluteUrlIfNeeded(conduitName, urlString, newURL, outMessage);
detectRedirectLoop(conduitName, urlString, newURL, outMessage);
checkAllowedRedirectUri(conduitName, urlString, newURL, outMessage);
} catch (IOException ex) {
// Consider introducing ClientRedirectException instead - it will require
// those client runtimes which want to check for it have a direct link to it
outMessage.getExchange().put("client.redirect.exception", "true");
throw ex;
}
if (newURL != null) {
new Headers(outMessage).removeAuthorizationHeaders();
// If user configured this Conduit with preemptive authorization
// it is meant to make it to the end. (Too bad that information
// went to every URL along the way, but that's what the user
// wants!
try {
setHeadersByAuthorizationPolicy(outMessage, new URI(newURL));
} catch (URISyntaxException e) {
throw new IOException(e);
}
cookies.writeToMessageHeaders(outMessage);
outMessage.put("transport.retransmit.url", newURL);
retransmit(newURL);
return true;
}
return false;
}
/**
* This method performs a retransmit for authorization information.
*
* @return true if there was a retransmit
* @throws IOException
*/
protected boolean authorizationRetransmit() throws IOException {
Message m = new MessageImpl();
updateResponseHeaders(m);
List<String> authHeaderValues = Headers.getSetProtocolHeaders(m).get("WWW-Authenticate");
if (authHeaderValues == null) {
LOG.warning("WWW-Authenticate response header is not set");
return false;
}
HttpAuthHeader authHeader = new HttpAuthHeader(authHeaderValues);
URI currentURI = url;
String realm = authHeader.getRealm();
detectAuthorizationLoop(getConduitName(), outMessage, currentURI, realm);
AuthorizationPolicy effectiveAthPolicy = getEffectiveAuthPolicy(outMessage);
String authorizationToken =
authSupplier.getAuthorization(
effectiveAthPolicy, currentURI, outMessage, authHeader.getFullHeader());
if (authorizationToken == null) {
// authentication not possible => we give up
return false;
}
try {
closeInputStream();
} catch (Throwable t) {
//ignore
}
new Headers(outMessage).setAuthorization(authorizationToken);
cookies.writeToMessageHeaders(outMessage);
retransmit(url.toString());
return true;
}
private int getMaxRetransmits() {
HTTPClientPolicy policy = getClient(outMessage);
// Default MaxRetransmits is -1 which means unlimited.
return (policy == null) ? -1 : policy.getMaxRetransmits();
}
/**
* This procedure is called on the close of the output stream so
* we are ready to handle the response from the connection.
* We may retransmit until we finally get a response.
*
* @throws IOException
*/
protected void handleResponse() throws IOException {
// Process retransmits until we fall out.
handleRetransmits();
if (outMessage == null
|| outMessage.getExchange() == null
|| outMessage.getExchange().isSynchronous()) {
handleResponseInternal();
} else {
handleResponseAsync();
}
}
/**
* This predicate returns true if the exchange indicates
* a oneway MEP.
*
* @param exchange The exchange in question
*/
private boolean isOneway(Exchange exchange) {
return exchange != null && exchange.isOneWay();
}
private boolean doProcessResponse(Message message, int responseCode) {
// 1. Not oneWay
if (!isOneway(message.getExchange())) {
return true;
}
// 2. Robust OneWays could have a fault
return responseCode == 500 && MessageUtils.getContextualBoolean(message, Message.ROBUST_ONEWAY, false);
}
protected int doProcessResponseCode() throws IOException {
Exchange exchange = outMessage.getExchange();
int rc = getResponseCode();
if (rc == -1) {
LOG.warning("HTTP Response code appears to be corrupted");
}
if (exchange != null) {
exchange.put(Message.RESPONSE_CODE, rc);
if (rc == 404 || rc == 503 || rc == 429) {
exchange.put("org.apache.cxf.transport.service_not_available", true);
}
}
// "org.apache.cxf.transport.no_io_exceptions" property should be set in case the exceptions
// should not be handled here; for example jax rs uses this
// "org.apache.cxf.transport.process_fault_on_http_400" property should be set in case a
// soap fault because of a HTTP 400 should be returned back to the client (SOAP 1.2 spec)
if (rc >= 400 && rc != 500
&& !MessageUtils.getContextualBoolean(outMessage, NO_IO_EXCEPTIONS)
&& (rc > 400 || !MessageUtils.getContextualBoolean(outMessage, PROCESS_FAULT_ON_HTTP_400))) {
throw new HTTPException(rc, getResponseMessage(), url.toURL());
}
return rc;
}
protected void handleResponseInternal() throws IOException {
Exchange exchange = outMessage.getExchange();
int responseCode = doProcessResponseCode();
InputStream in = null;
// oneway or decoupled twoway calls may expect HTTP 202 with no content
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
updateResponseHeaders(inMessage);
inMessage.put(Message.RESPONSE_CODE, responseCode);
if (MessageUtils.getContextualBoolean(outMessage, SET_HTTP_RESPONSE_MESSAGE, false)) {
inMessage.put(HTTP_RESPONSE_MESSAGE, getResponseMessage());
}
propagateConduit(exchange, inMessage);
if ((!doProcessResponse(outMessage, responseCode)
|| HttpURLConnection.HTTP_ACCEPTED == responseCode)
&& MessageUtils.getContextualBoolean(outMessage,
Message.PROCESS_202_RESPONSE_ONEWAY_OR_PARTIAL, true)) {
in = getPartialResponse();
if (in == null
|| !MessageUtils.getContextualBoolean(outMessage, Message.PROCESS_ONEWAY_RESPONSE, false)) {
// oneway operation or decoupled MEP without
// partial response
closeInputStream();
if (isOneway(exchange) && responseCode > 300) {
throw new HTTPException(responseCode, getResponseMessage(), url.toURL());
}
//REVISIT move the decoupled destination property name into api
Endpoint ep = exchange.getEndpoint();
if (null != ep && null != ep.getEndpointInfo() && null == ep.getEndpointInfo().
getProperty("org.apache.cxf.ws.addressing.MAPAggregator.decoupledDestination")) {
// remove callback so that it won't be invoked twice
ClientCallback cc = exchange.remove(ClientCallback.class);
if (null != cc) {
cc.handleResponse(null, null);
}
}
exchange.put("IN_CHAIN_COMPLETE", Boolean.TRUE);
exchange.setInMessage(inMessage);
return;
}
} else {
//not going to be resending or anything, clear out the stuff in the out message
//to free memory
outMessage.removeContent(OutputStream.class);
if (cachingForRetransmission && cachedStream != null) {
cachedStream.close();
}
cachedStream = null;
}
String charset = HttpHeaderHelper.findCharset((String)inMessage.get(Message.CONTENT_TYPE));
String normalizedEncoding = HttpHeaderHelper.mapCharset(charset);
if (normalizedEncoding == null) {
String m = new org.apache.cxf.common.i18n.Message("INVALID_ENCODING_MSG",
LOG, charset).toString();
LOG.log(Level.WARNING, m);
throw new IOException(m);
}
inMessage.put(Message.ENCODING, normalizedEncoding);
if (in == null) {
in = getInputStream();
}
if (in == null) {
// Create an empty stream to avoid NullPointerExceptions
in = new ByteArrayInputStream(new byte[] {});
}
inMessage.setContent(InputStream.class, in);
incomingObserver.onMessage(inMessage);
}
protected void propagateConduit(Exchange exchange, Message in) {
if (exchange != null) {
Message out = exchange.getOutMessage();
if (out != null) {
in.put(Conduit.class, out.get(Conduit.class));
}
}
}
protected void handleHttpRetryException(HttpRetryException e) throws IOException {
String msg = "HTTP response '" + e.responseCode() + ": "
+ getResponseMessage() + "' invoking " + url;
switch (e.responseCode()) {
case HttpURLConnection.HTTP_MOVED_PERM: // 301
case HttpURLConnection.HTTP_MOVED_TEMP: // 302
case HttpURLConnection.HTTP_SEE_OTHER: // 303
case 307:
msg += " that returned location header '" + e.getLocation() + "'";
break;
case HttpURLConnection.HTTP_UNAUTHORIZED: // 401
if (authorizationPolicy == null || authorizationPolicy.getUserName() == null) {
msg += " with NO authorization username configured in conduit " + getConduitName();
} else {
msg += " with authorization username '" + authorizationPolicy.getUserName() + "'";
}
break;
case HttpURLConnection.HTTP_PROXY_AUTH: // 407
if (proxyAuthorizationPolicy == null || proxyAuthorizationPolicy.getUserName() == null) {
msg += " with NO proxy authorization configured in conduit " + getConduitName();
} else {
msg += " with proxy authorization username '"
+ proxyAuthorizationPolicy.getUserName() + "'";
}
if (clientSidePolicy == null || clientSidePolicy.getProxyServer() == null) {
if (usingProxy()) {
msg += " using a proxy even if NONE is configured in CXF conduit "
+ getConduitName()
+ " (maybe one is configured by java.net.ProxySelector)";
} else {
msg += " but NO proxy was used by the connection (none configured in cxf "
+ "conduit and none selected by java.net.ProxySelector)";
}
} else {
msg += " using " + clientSidePolicy.getProxyServerType() + " proxy "
+ clientSidePolicy.getProxyServer() + ":"
+ clientSidePolicy.getProxyServerPort();
}
break;
default:
// No other type of HttpRetryException should be thrown
break;
}
throw new IOException(msg, e);
}
/**
* This call must take place before anything is written to the
* URLConnection. The URLConnection.connect() will be called in order
* to get the connection information.
*
* This method is invoked just after setURLRequestHeaders() from the
* WrappedOutputStream before it writes data to the URLConnection.
*
* If trust cannot be established the Trust Decider implemenation
* throws an IOException.
*
* @throws IOException This exception is thrown if trust cannot be
* established by the configured MessageTrustDecider.
* @see MessageTrustDecider
*/
protected void makeTrustDecision() throws IOException {
MessageTrustDecider decider2 = outMessage.get(MessageTrustDecider.class);
if (trustDecider != null || decider2 != null) {
try {
// We must connect or we will not get the credentials.
// The call is (said to be) ignored internally if
// already connected.
HttpsURLConnectionInfo info = getHttpsURLConnectionInfo();
if (trustDecider != null) {
trustDecider.establishTrust(conduitName,
info,
outMessage);
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Trust Decider "
+ trustDecider.getLogicalName()
+ " considers Conduit "
+ conduitName
+ " trusted.");
}
}
if (decider2 != null) {
decider2.establishTrust(conduitName,
info,
outMessage);
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Trust Decider "
+ decider2.getLogicalName()
+ " considers Conduit "
+ conduitName
+ " trusted.");
}
}
} catch (UntrustedURLConnectionIOException untrustedEx) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Trust Decider "
+ (trustDecider != null ? trustDecider.getLogicalName() : decider2.getLogicalName())
+ " considers Conduit "
+ conduitName
+ " untrusted.", untrustedEx);
}
throw untrustedEx;
}
} else {
// This case, when there is no trust decider, a trust
// decision should be a matter of policy.
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "No Trust Decider for Conduit '"
+ conduitName
+ "'. An affirmative Trust Decision is assumed.");
}
}
}
}
private static void checkAllowedRedirectUri(String conduitName,
String lastURL,
String newURL,
Message message) throws IOException {
if (newURL != null) {
URI newUri = URI.create(newURL);
if (MessageUtils.getContextualBoolean(message, AUTO_REDIRECT_SAME_HOST_ONLY)) {
URI lastUri = URI.create(lastURL);
// This can be further restricted to make sure newURL completely contains lastURL
// though making sure the same HTTP scheme and host are preserved should be enough
if (!newUri.getScheme().equals(lastUri.getScheme())
|| !newUri.getHost().equals(lastUri.getHost())) {
String msg = "Different HTTP Scheme or Host Redirect detected on Conduit '"
+ conduitName + "' on '" + newURL + "'";
LOG.log(Level.INFO, msg);
throw new IOException(msg);
}
}
String allowedRedirectURI = (String)message.getContextualProperty(AUTO_REDIRECT_ALLOWED_URI);
if (allowedRedirectURI != null && !newURL.startsWith(allowedRedirectURI)) {
String msg = "Forbidden Redirect URI " + newURL + "detected on Conduit '" + conduitName;
LOG.log(Level.INFO, msg);
throw new IOException(msg);
}
}
}
// http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-23#section-7.1.2
// Relative Location values are also supported
private static String convertToAbsoluteUrlIfNeeded(String conduitName,
String lastURL,
String newURL,
Message message) throws IOException {
if (newURL != null && !newURL.startsWith("http")) {
if (MessageUtils.getContextualBoolean(message, AUTO_REDIRECT_ALLOW_REL_URI)) {
return URI.create(lastURL).resolve(newURL).toString();
}
String msg = "Relative Redirect detected on Conduit '"
+ conduitName + "' on '" + newURL + "'";
LOG.log(Level.INFO, msg);
throw new IOException(msg);
}
return newURL;
}
private static void detectRedirectLoop(String conduitName,
String lastURL,
String newURL,
Message message) throws IOException {
Map<String, Integer> visitedURLs = CastUtils.cast((Map<?, ?>)message.get(KEY_VISITED_URLS));
if (visitedURLs == null) {
visitedURLs = new HashMap<>();
message.put(KEY_VISITED_URLS, visitedURLs);
}
Integer visitCount = visitedURLs.get(lastURL);
if (visitCount == null) {
visitCount = 1;
} else {
visitCount++;
}
visitedURLs.put(lastURL, visitCount);
Integer newURLCount = visitedURLs.get(newURL);
if (newURL != null && newURLCount != null) {
// See if we are being redirected in a loop as best we can,
// using string equality on URL.
boolean invalidLoopDetected = newURL.equals(lastURL);
Integer maxSameURICount = PropertyUtils.getInteger(message, AUTO_REDIRECT_MAX_SAME_URI_COUNT);
if (!invalidLoopDetected) {
// This new URI was already recorded earlier even though it is not equal to the last URI
// Example: a-b-a, where 'a' is the new URI. Check if a limited number of occurrences of this URI
// is allowed, fail by default.
if (maxSameURICount == null || newURLCount > maxSameURICount) {
invalidLoopDetected = true;
}
} else if (maxSameURICount != null && newURLCount <= maxSameURICount) {
// This new URI was already recorded earlier and is the same as the last URI.
// Example: a-a. But we have a property supporting a limited number of occurrences of this URI.
// Continue the invocation.
invalidLoopDetected = false;
}
if (invalidLoopDetected) {
// We are in a redirect loop; -- bail
String msg = "Redirect loop detected on Conduit '"
+ conduitName + "' on '" + newURL + "'";
LOG.log(Level.INFO, msg);
throw new IOException(msg);
}
}
}
private static void detectAuthorizationLoop(String conduitName, Message message,
URI currentURL, String realm) throws IOException {
@SuppressWarnings("unchecked")
Set<String> authURLs = (Set<String>) message.get(KEY_AUTH_URLS);
if (authURLs == null) {
authURLs = new HashSet<>();
message.put(KEY_AUTH_URLS, authURLs);
}
// If we have been here (URL & Realm) before for this particular message
// retransmit, it means we have already supplied information
// which must have been wrong, or we wouldn't be here again.
// Otherwise, the server may be 401 looping us around the realms.
if (!authURLs.add(currentURL.toString() + realm)) {
String logMessage = "Authorization loop detected on Conduit \""
+ conduitName
+ "\" on URL \""
+ currentURL
+ "\" with realm \""
+ realm
+ "\"";
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, logMessage);
}
throw new IOException(logMessage);
}
}
}