Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.transport.http.Address;
import org.apache.cxf.transport.http.HTTPException;
import org.apache.cxf.transport.http.Headers;
import org.apache.cxf.transport.http.HttpClientHTTPConduit;
import org.apache.cxf.transport.http.asyncclient.hc5.AsyncHTTPConduitFactory.UseAsyncPolicy;
Expand Down Expand Up @@ -300,6 +301,7 @@ public class AsyncWrappedOutputStream extends WrappedOutputStream

private Object sessionLock = new Object();
private boolean closed;
private HttpClientContext ctx;

public AsyncWrappedOutputStream(Message message, boolean needToCacheRequest, boolean isChunking,
int chunkThreshold, String conduitName, URI uri) {
Expand Down Expand Up @@ -531,7 +533,7 @@ public void cancelled() {
entity.setEntity(null);
}

HttpClientContext ctx = HttpClientContext.create();
ctx = HttpClientContext.create();

BasicCredentialsProvider credsProvider = new BasicCredentialsProvider() {
@Override
Expand Down Expand Up @@ -673,7 +675,12 @@ protected synchronized HttpResponse getHttpResponse() throws IOException {
while (httpResponse == null) {
if (exception == null) { //already have an exception, skip waiting
try {
wait();
long timeout = csPolicy.getReceiveTimeout();
if (timeout > 0) {
wait(timeout);
} else {
wait();
}
} catch (InterruptedException e) {
throw new IOException(e);
}
Expand All @@ -686,12 +693,34 @@ protected synchronized HttpResponse getHttpResponse() throws IOException {
if (exception instanceof IOException) {
throw (IOException)exception;
} else if (exception instanceof RuntimeException) {
throw (RuntimeException)exception;
// HC5 async calls failed() with RuntimeException
// when proxy auth is exhausted, not 407 via
// consumeResponse(). Check context for 407.
if (ctx != null && ctx.getResponse() != null
&& ctx.getResponse().getCode()
== HttpURLConnection.HTTP_PROXY_AUTH) {
throw new HTTPException(
HttpURLConnection.HTTP_PROXY_AUTH,
ctx.getResponse().getReasonPhrase(),
url.toURL());
}
throw new IOException(exception);
}

throw new IOException(exception);
}

// Timed out waiting for HC5 callback; check if proxy
// returned 407 during its internal auth handling.
if (ctx != null && ctx.getResponse() != null
&& ctx.getResponse().getCode()
== HttpURLConnection.HTTP_PROXY_AUTH) {
throw new HTTPException(
HttpURLConnection.HTTP_PROXY_AUTH,
ctx.getResponse().getReasonPhrase(),
url.toURL());
}

throw new SocketTimeoutException("Read Timeout");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* 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.systest.hc5.http.auth;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import javax.xml.namespace.QName;

import jakarta.xml.ws.BindingProvider;
import jakarta.xml.ws.WebServiceException;
import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transport.http.HTTPException;
import org.apache.cxf.transport.http.asyncclient.hc5.AsyncHTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.hello_world.Greeter;
import org.apache.hello_world.services.SOAPService;

import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

/**
* Reproducer for CXF-9226 (connection-close + retransmit variant): async
* HC5 conduit throws IllegalStateException instead of HTTPException(407)
* when the proxy returns 407, closes the TCP connection (Connection: close),
* and the client has MaxRetransmits/AutoRedirect configured (as in the JIRA).
*
* <p>With MaxRetransmits &gt; 0 or AutoRedirect=true, CXF caches the request
* body so HC5 marks the entity as repeatable. HC5 then retries after the
* first 407, reconnects to the proxy, and sends a second request with the
* (wrong) Basic credentials. While HC5 is reconnecting, a concurrent
* write-failure on the closed socket calls discardEndpoint() which sets
* endpointRef to null. When the retry then calls execute() →
* ensureValid(), it finds endpointRef==null and throws
* IllegalStateException("Endpoint not acquired / already released").
*
* <p>Without the CXF-9226 fix that ISE surfaces to the caller as an opaque
* IOException; with the fix CXF checks the 407 stored in HttpClientContext
* and throws HTTPException(407) instead.
*/
public class ProxyAuthIllegalStateTest
extends AbstractBusClientServerTestBase {

static final String PROXY_PORT =
allocatePort(ProxyAuthIllegalStateTest.class);

private static final QName SERVICE_NAME =
new QName("http://apache.org/hello_world", "SOAPService");
private static final QName PORT_NAME =
new QName("http://apache.org/hello_world", "Mortimer");

private static ServerSocket proxySocket;

@BeforeClass
public static void setup() throws Exception {
proxySocket = new ServerSocket(Integer.parseInt(PROXY_PORT));
final ServerSocket socket = proxySocket;
Thread t = new Thread(() -> {
while (!socket.isClosed()) {
try {
drainAndReject(socket.accept());
} catch (IOException e) {
// socket closed; exit loop
}
}
});
t.setDaemon(true);
t.start();
createStaticBus();
}

private static void drainAndReject(Socket client) throws IOException {
try (Socket s = client) {
InputStream in = s.getInputStream();
// Slide a 4-byte window; stop at \r\n\r\n (end of headers)
int[] w = new int[]{-1, -1, -1, -1};
int b;
while ((b = in.read()) != -1) {
System.arraycopy(w, 1, w, 0, 3);
w[3] = b;
if (w[0] == '\r' && w[1] == '\n'
&& w[2] == '\r' && w[3] == '\n') {
break;
}
}
OutputStream out = s.getOutputStream();
out.write(("HTTP/1.1 407 Proxy Authentication Required\r\n"
+ "Proxy-Authenticate: Basic realm=\"test\"\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: close\r\n"
+ "\r\n").getBytes(StandardCharsets.US_ASCII));
out.flush();
}
}

@AfterClass
public static void teardown() {
if (proxySocket != null) {
try {
proxySocket.close();
} catch (IOException e) {
// ignore
}
proxySocket = null;
}
}

/**
* CXF-9226: wrong proxy credentials must produce HTTPException(407),
* not IllegalStateException, when the proxy closes the connection and
* MaxRetransmits/AutoRedirect are configured (exact JIRA reproduction).
*/
@Test(timeout = 30_000)
public void testProxyAuthFailsWithHTTPExceptionNotIllegalState()
throws Exception {
Assume.assumeFalse("Skipped in forceURLConnection mode",
Boolean.getBoolean(
"org.apache.cxf.transport.http.forceURLConnection"));
URL wsdl = getClass().getResource("../greeting.wsdl");
assertNotNull("WSDL not found", wsdl);

SOAPService service = new SOAPService(wsdl, SERVICE_NAME);
Greeter greeter = service.getPort(PORT_NAME, Greeter.class);
assertNotNull("Port is null", greeter);

BindingProvider bp = (BindingProvider) greeter;
bp.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
"http://localhost:1/greeter");
bp.getRequestContext().put(AsyncHTTPConduit.USE_ASYNC, Boolean.TRUE);

HTTPConduit conduit = (HTTPConduit)
ClientProxy.getClient(greeter).getConduit();
HTTPClientPolicy policy = new HTTPClientPolicy();
policy.setConnectionTimeout(10_000L);
policy.setReceiveTimeout(10_000L);
policy.setProxyServer("localhost");
policy.setProxyServerPort(Integer.parseInt(PROXY_PORT));
// Match the exact JIRA configuration: MaxRetransmits and AutoRedirect
// force CXF to cache the request body, which makes the entity
// repeatable. HC5 then retries after the 407, and the concurrent
// write-failure on the Connection: close socket can set endpointRef=null
// before the retry's execute() call, triggering IllegalStateException.
policy.setMaxRetransmits(5);
policy.setAutoRedirect(true);
conduit.setClient(policy);

ProxyAuthorizationPolicy proxyAuth = new ProxyAuthorizationPolicy();
proxyAuth.setUserName("unknown-user");
proxyAuth.setPassword("wrong-password");
proxyAuth.setAuthorizationType("Basic");
conduit.setProxyAuthorization(proxyAuth);

try {
greeter.sayHi();
fail("Expected exception for invalid proxy credentials");
} catch (WebServiceException e) {
Throwable cause = e.getCause();
assertNotNull("Expected a cause on the WebServiceException", cause);
if (!(cause instanceof HTTPException)) {
fail("Expected HTTPException(407) but got "
+ cause.getClass().getName() + ": " + cause.getMessage());
}
assertEquals("Expected 407 Proxy Authentication Required",
407, ((HTTPException) cause).getResponseCode());
}
}
}