Skip to content

Commit 4d64e04

Browse files
ffangreta
andauthored
CXF-9226: AsyncHTTPConduit throws ISE instead of HTTPException(407) o… (#3255)
* CXF-9226: AsyncHTTPConduit throws ISE instead of HTTPException(407) on proxy auth failure When the HC5 async conduit receives a 407 Proxy Authentication Required response with wrong credentials and MaxRetransmits>0 or AutoRedirect=true (as in the JIRA), the request entity is marked repeatable so HC5 retries after the first 407. A concurrent write-failure on the Connection: close socket races with the retry's execute() call, causing InternalHttpAsyncExecRuntime.ensureValid() to find a null endpointRef and throw IllegalStateException. Without MaxRetransmits/AutoRedirect the entity is not repeatable and HC5 intercepts the 407 internally, calling asyncExecCallback.completed() without signaling CXF's future and leaving getHttpResponse() blocked indefinitely on wait(). Fix: - Promote HttpClientContext ctx to a field on AsyncWrappedOutputStream so it is accessible after HC5 calls back. - Replace wait() with wait(receiveTimeout) to bound the wait. - In both the timeout path and the RuntimeException (ISE) path, check ctx.getResponse() for a 407 status and throw HTTPException(407) instead of re-throwing the raw exception or blocking forever. Test: - ProxyAuthIllegalStateTest: reproduces the ISE via a raw ServerSocket proxy returning 407+Connection:close, with MaxRetransmits=5 and AutoRedirect=true matching the exact JIRA configuration. * Always configure AuthCache if proxy authorization policy is configured * Use exact proxy host and BasicAuthCache implementation --------- Co-authored-by: Andriy Redko <drreta@gmail.com>
1 parent 71a342c commit 4d64e04

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

rt/transports/http-hc5/src/main/java/org/apache/cxf/transport/http/asyncclient/hc5/AsyncHTTPConduit.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@
7878
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
7979
import org.apache.hc.client5.http.config.RequestConfig;
8080
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
81+
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
8182
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
83+
import org.apache.hc.client5.http.impl.auth.BasicScheme;
8284
import org.apache.hc.client5.http.protocol.HttpClientContext;
8385
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
8486
import org.apache.hc.core5.concurrent.BasicFuture;
@@ -553,6 +555,11 @@ public Credentials getCredentials(final AuthScope authscope, HttpContext context
553555
};
554556

555557
ctx.setCredentialsProvider(credsProvider);
558+
if (proxyAuthorizationPolicy != null && proxyAuthorizationPolicy.getUserName() != null) {
559+
final BasicAuthCache cache = new BasicAuthCache();
560+
cache.put(entity.getConfig().getProxy(), new BasicScheme());
561+
ctx.setAuthCache(cache);
562+
}
556563

557564
TlsStrategy tlsStrategy = null;
558565
if ("https".equals(url.getScheme())) {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.cxf.systest.hc5.http.auth;
20+
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.io.OutputStream;
24+
import java.net.ServerSocket;
25+
import java.net.Socket;
26+
import java.net.URL;
27+
import java.nio.charset.StandardCharsets;
28+
29+
import javax.xml.namespace.QName;
30+
31+
import jakarta.xml.ws.BindingProvider;
32+
import jakarta.xml.ws.WebServiceException;
33+
import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
34+
import org.apache.cxf.frontend.ClientProxy;
35+
import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
36+
import org.apache.cxf.transport.http.HTTPConduit;
37+
import org.apache.cxf.transport.http.HTTPException;
38+
import org.apache.cxf.transport.http.asyncclient.hc5.AsyncHTTPConduit;
39+
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
40+
import org.apache.hello_world.Greeter;
41+
import org.apache.hello_world.services.SOAPService;
42+
43+
import org.junit.AfterClass;
44+
import org.junit.Assume;
45+
import org.junit.BeforeClass;
46+
import org.junit.Test;
47+
48+
import static org.junit.Assert.assertEquals;
49+
import static org.junit.Assert.assertNotNull;
50+
import static org.junit.Assert.fail;
51+
52+
/**
53+
* Reproducer for CXF-9226 (connection-close + retransmit variant): async
54+
* HC5 conduit throws IllegalStateException instead of HTTPException(407)
55+
* when the proxy returns 407, closes the TCP connection (Connection: close),
56+
* and the client has MaxRetransmits/AutoRedirect configured (as in the JIRA).
57+
*
58+
* <p>With MaxRetransmits &gt; 0 or AutoRedirect=true, CXF caches the request
59+
* body so HC5 marks the entity as repeatable. HC5 then retries after the
60+
* first 407, reconnects to the proxy, and sends a second request with the
61+
* (wrong) Basic credentials. While HC5 is reconnecting, a concurrent
62+
* write-failure on the closed socket calls discardEndpoint() which sets
63+
* endpointRef to null. When the retry then calls execute() →
64+
* ensureValid(), it finds endpointRef==null and throws
65+
* IllegalStateException("Endpoint not acquired / already released").
66+
*
67+
* <p>Without the CXF-9226 fix that ISE surfaces to the caller as an opaque
68+
* IOException; with the fix CXF checks the 407 stored in HttpClientContext
69+
* and throws HTTPException(407) instead.
70+
*/
71+
public class ProxyAuthIllegalStateTest
72+
extends AbstractBusClientServerTestBase {
73+
74+
static final String PROXY_PORT =
75+
allocatePort(ProxyAuthIllegalStateTest.class);
76+
77+
private static final QName SERVICE_NAME =
78+
new QName("http://apache.org/hello_world", "SOAPService");
79+
private static final QName PORT_NAME =
80+
new QName("http://apache.org/hello_world", "Mortimer");
81+
82+
private static ServerSocket proxySocket;
83+
84+
@BeforeClass
85+
public static void setup() throws Exception {
86+
proxySocket = new ServerSocket(Integer.parseInt(PROXY_PORT));
87+
final ServerSocket socket = proxySocket;
88+
Thread t = new Thread(() -> {
89+
while (!socket.isClosed()) {
90+
try {
91+
drainAndReject(socket.accept());
92+
} catch (IOException e) {
93+
// socket closed; exit loop
94+
}
95+
}
96+
});
97+
t.setDaemon(true);
98+
t.start();
99+
createStaticBus();
100+
}
101+
102+
private static void drainAndReject(Socket client) throws IOException {
103+
try (Socket s = client) {
104+
InputStream in = s.getInputStream();
105+
// Slide a 4-byte window; stop at \r\n\r\n (end of headers)
106+
int[] w = new int[]{-1, -1, -1, -1};
107+
int b;
108+
while ((b = in.read()) != -1) {
109+
System.arraycopy(w, 1, w, 0, 3);
110+
w[3] = b;
111+
if (w[0] == '\r' && w[1] == '\n'
112+
&& w[2] == '\r' && w[3] == '\n') {
113+
break;
114+
}
115+
}
116+
OutputStream out = s.getOutputStream();
117+
out.write(("HTTP/1.1 407 Proxy Authentication Required\r\n"
118+
+ "Proxy-Authenticate: Basic realm=\"test\"\r\n"
119+
+ "Content-Length: 0\r\n"
120+
+ "Connection: close\r\n"
121+
+ "\r\n").getBytes(StandardCharsets.US_ASCII));
122+
out.flush();
123+
}
124+
}
125+
126+
@AfterClass
127+
public static void teardown() {
128+
if (proxySocket != null) {
129+
try {
130+
proxySocket.close();
131+
} catch (IOException e) {
132+
// ignore
133+
}
134+
proxySocket = null;
135+
}
136+
}
137+
138+
/**
139+
* CXF-9226: wrong proxy credentials must produce HTTPException(407),
140+
* not IllegalStateException, when the proxy closes the connection and
141+
* MaxRetransmits/AutoRedirect are configured (exact JIRA reproduction).
142+
*/
143+
@Test(timeout = 30_000)
144+
public void testProxyAuthFailsWithHTTPExceptionNotIllegalState()
145+
throws Exception {
146+
Assume.assumeFalse("Skipped in forceURLConnection mode",
147+
Boolean.getBoolean(
148+
"org.apache.cxf.transport.http.forceURLConnection"));
149+
URL wsdl = getClass().getResource("../greeting.wsdl");
150+
assertNotNull("WSDL not found", wsdl);
151+
152+
SOAPService service = new SOAPService(wsdl, SERVICE_NAME);
153+
Greeter greeter = service.getPort(PORT_NAME, Greeter.class);
154+
assertNotNull("Port is null", greeter);
155+
156+
BindingProvider bp = (BindingProvider) greeter;
157+
bp.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
158+
"http://localhost:1/greeter");
159+
bp.getRequestContext().put(AsyncHTTPConduit.USE_ASYNC, Boolean.TRUE);
160+
161+
HTTPConduit conduit = (HTTPConduit)
162+
ClientProxy.getClient(greeter).getConduit();
163+
HTTPClientPolicy policy = new HTTPClientPolicy();
164+
policy.setConnectionTimeout(10_000L);
165+
policy.setReceiveTimeout(10_000L);
166+
policy.setProxyServer("localhost");
167+
policy.setProxyServerPort(Integer.parseInt(PROXY_PORT));
168+
// Match the exact JIRA configuration: MaxRetransmits and AutoRedirect
169+
// force CXF to cache the request body, which makes the entity
170+
// repeatable. HC5 then retries after the 407, and the concurrent
171+
// write-failure on the Connection: close socket can set endpointRef=null
172+
// before the retry's execute() call, triggering IllegalStateException.
173+
policy.setMaxRetransmits(5);
174+
policy.setAutoRedirect(true);
175+
conduit.setClient(policy);
176+
177+
ProxyAuthorizationPolicy proxyAuth = new ProxyAuthorizationPolicy();
178+
proxyAuth.setUserName("unknown-user");
179+
proxyAuth.setPassword("wrong-password");
180+
proxyAuth.setAuthorizationType("Basic");
181+
conduit.setProxyAuthorization(proxyAuth);
182+
183+
try {
184+
greeter.sayHi();
185+
fail("Expected exception for invalid proxy credentials");
186+
} catch (WebServiceException e) {
187+
Throwable cause = e.getCause();
188+
assertNotNull("Expected a cause on the WebServiceException", cause);
189+
if (!(cause instanceof HTTPException)) {
190+
fail("Expected HTTPException(407) but got "
191+
+ cause.getClass().getName() + ": " + cause.getMessage());
192+
}
193+
assertEquals("Expected 407 Proxy Authentication Required",
194+
407, ((HTTPException) cause).getResponseCode());
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)