ProxyAuthIllegalStateTest.java
/**
* 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 > 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());
}
}
}