Skip to content

Commit 777fe72

Browse files
Copilotchrjohn
andcommitted
Add HTTP proxy authentication with HttpSmartProxyHandler
- Add imports for Base64, Collections, StandardCharsets, List, Map, HttpSmartProxyHandler - Add PROXY_AUTHORIZATION_HEADER constant - Implement proactive Proxy-Authorization header for Basic auth in createHttpProxyRequest() - Only set header when username/password provided without domain/workstation - NTLM authentication uses multi-step handshake without proactive header - Add HttpSmartProxyHandler configuration in createIoProxyConnector() - Handler set for all HTTP proxy connections - Works with proactive header and handles 407 challenge-response - Add comprehensive unit tests: - Basic auth: verifies header and handler - NTLM auth: verifies no header, handler is set, properties configured - No credentials: verifies no header, handler is set - All 4 tests passing Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com>
1 parent b753057 commit 777fe72

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121

2222
import java.net.InetSocketAddress;
2323
import java.net.SocketAddress;
24+
import java.nio.charset.StandardCharsets;
25+
import java.util.Base64;
26+
import java.util.Collections;
2427
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
2530

2631

2732
import org.apache.mina.core.service.IoAcceptor;
@@ -36,6 +41,7 @@
3641
import org.apache.mina.proxy.handlers.ProxyRequest;
3742
import org.apache.mina.proxy.handlers.http.HttpProxyConstants;
3843
import org.apache.mina.proxy.handlers.http.HttpProxyRequest;
44+
import org.apache.mina.proxy.handlers.http.HttpSmartProxyHandler;
3945
import org.apache.mina.proxy.handlers.socks.SocksProxyConstants;
4046
import org.apache.mina.proxy.handlers.socks.SocksProxyRequest;
4147
import org.apache.mina.proxy.session.ProxyIoSession;
@@ -52,6 +58,8 @@ public class ProtocolFactory {
5258

5359
public final static int SOCKET = 0;
5460
public final static int VM_PIPE = 1;
61+
62+
public final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
5563

5664
public static String getTypeString(int type) {
5765
switch (type) {
@@ -133,6 +141,14 @@ public static ProxyConnector createIoProxyConnector(SocketConnector socketConnec
133141
}
134142

135143
ProxyIoSession proxyIoSession = new ProxyIoSession(proxyAddress, req);
144+
145+
// Explicitly set HttpSmartProxyHandler for HTTP proxies to properly manage authentication
146+
// This handler works with the proactive Proxy-Authorization header set in the request
147+
// and can also handle 407 challenge-response if needed
148+
if (proxyType.equalsIgnoreCase("http")) {
149+
proxyIoSession.setHandler(new HttpSmartProxyHandler(proxyIoSession));
150+
}
151+
136152
connector.setProxyIoSession(proxyIoSession);
137153

138154
return connector;
@@ -155,12 +171,26 @@ private static ProxyRequest createHttpProxyRequest(InetSocketAddress address,
155171

156172
HttpProxyRequest req = new HttpProxyRequest(address);
157173
req.setProperties(props);
174+
158175
if (proxyVersion != null && proxyVersion.equalsIgnoreCase("1.1")) {
159176
req.setHttpVersion(HttpProxyConstants.HTTP_1_1);
160177
} else {
161178
req.setHttpVersion(HttpProxyConstants.HTTP_1_0);
162179
}
163180

181+
// Set Proxy-Authorization header for Basic authentication if credentials are provided
182+
// Some proxy servers require this header to be set upfront rather than waiting for a 407 response
183+
// Note: NTLM authentication requires a multi-step handshake and should not set headers upfront
184+
if (proxyUser != null && !proxyUser.isEmpty()
185+
&& proxyPassword != null && !proxyPassword.isEmpty()
186+
&& proxyDomain == null && proxyWorkstation == null) {
187+
Map<String, List<String>> headers = new HashMap<>();
188+
String credentials = proxyUser + ":" + proxyPassword;
189+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
190+
headers.put(PROXY_AUTHORIZATION_HEADER, Collections.singletonList("Basic " + encodedCredentials));
191+
req.setHeaders(headers);
192+
}
193+
164194
return req;
165195
}
166196

quickfixj-core/src/test/java/quickfix/mina/ProtocolFactoryTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,24 @@
2121

2222
import org.apache.mina.core.service.IoConnector;
2323
import org.apache.mina.proxy.ProxyConnector;
24+
import org.apache.mina.proxy.handlers.http.HttpProxyRequest;
25+
import org.apache.mina.proxy.handlers.http.HttpSmartProxyHandler;
2426
import org.apache.mina.proxy.session.ProxyIoSession;
2527
import org.apache.mina.transport.socket.SocketConnector;
2628
import org.apache.mina.util.AvailablePortFinder;
2729
import org.junit.Test;
2830
import quickfix.ConfigError;
2931

3032
import java.net.InetSocketAddress;
33+
import java.util.Base64;
34+
import java.util.List;
35+
import java.util.Map;
3136

37+
import static org.junit.Assert.assertEquals;
38+
import static org.junit.Assert.assertNotNull;
3239
import static org.junit.Assert.assertNull;
40+
import static org.junit.Assert.assertTrue;
41+
import static quickfix.mina.ProtocolFactory.PROXY_AUTHORIZATION_HEADER;
3342

3443
public class ProtocolFactoryTest {
3544

@@ -46,4 +55,95 @@ public void shouldCreateProxyConnectorWithoutPreferredAuthOrder() throws ConfigE
4655
ProxyIoSession proxySession = proxyConnector.getProxyIoSession();
4756
assertNull(proxySession.getPreferedOrder());
4857
}
58+
59+
@Test
60+
public void shouldSetBasicAuthorizationHeaderForHttpProxy() throws ConfigError {
61+
InetSocketAddress address = new InetSocketAddress(AvailablePortFinder.getNextAvailable());
62+
InetSocketAddress proxyAddress = new InetSocketAddress(AvailablePortFinder.getNextAvailable());
63+
64+
IoConnector connector = ProtocolFactory.createIoConnector(address);
65+
ProxyConnector proxyConnector = ProtocolFactory
66+
.createIoProxyConnector((SocketConnector) connector, address, proxyAddress, "http", "1.0", "testuser",
67+
"testpassword", null, null);
68+
69+
ProxyIoSession proxySession = proxyConnector.getProxyIoSession();
70+
71+
// Verify HttpSmartProxyHandler is set
72+
assertNotNull("Handler should be set", proxySession.getHandler());
73+
assertTrue("Handler should be HttpSmartProxyHandler",
74+
proxySession.getHandler() instanceof HttpSmartProxyHandler);
75+
76+
HttpProxyRequest request = (HttpProxyRequest) proxySession.getRequest();
77+
78+
Map<String, List<String>> headers = request.getHeaders();
79+
assertNotNull("Headers should not be null", headers);
80+
assertTrue("Headers should contain Proxy-Authorization", headers.containsKey(PROXY_AUTHORIZATION_HEADER));
81+
82+
List<String> authHeaders = headers.get(PROXY_AUTHORIZATION_HEADER);
83+
assertNotNull("Proxy-Authorization header should not be null", authHeaders);
84+
assertEquals("Should have exactly one Proxy-Authorization header", 1, authHeaders.size());
85+
86+
String authHeader = authHeaders.get(0);
87+
assertTrue("Auth header should start with 'Basic '", authHeader.startsWith("Basic "));
88+
89+
// Verify the encoded credentials
90+
String encodedPart = authHeader.substring("Basic ".length());
91+
String decoded = new String(Base64.getDecoder().decode(encodedPart));
92+
assertEquals("Decoded credentials should match", "testuser:testpassword", decoded);
93+
}
94+
95+
@Test
96+
public void shouldNotSetAuthorizationHeaderForNTLMAuthentication() throws ConfigError {
97+
InetSocketAddress address = new InetSocketAddress(AvailablePortFinder.getNextAvailable());
98+
InetSocketAddress proxyAddress = new InetSocketAddress(AvailablePortFinder.getNextAvailable());
99+
100+
IoConnector connector = ProtocolFactory.createIoConnector(address);
101+
ProxyConnector proxyConnector = ProtocolFactory
102+
.createIoProxyConnector((SocketConnector) connector, address, proxyAddress, "http", "1.0", "testuser",
103+
"testpassword", "TESTDOMAIN", "TESTWORKSTATION");
104+
105+
ProxyIoSession proxySession = proxyConnector.getProxyIoSession();
106+
107+
// Verify HttpSmartProxyHandler is set for NTLM too
108+
assertNotNull("Handler should be set", proxySession.getHandler());
109+
assertTrue("Handler should be HttpSmartProxyHandler for NTLM",
110+
proxySession.getHandler() instanceof HttpSmartProxyHandler);
111+
112+
HttpProxyRequest request = (HttpProxyRequest) proxySession.getRequest();
113+
114+
Map<String, List<String>> headers = request.getHeaders();
115+
// NTLM requires multi-step handshake, so Proxy-Authorization header should not be set upfront
116+
assertTrue("Headers should be null or not contain Proxy-Authorization for NTLM",
117+
headers == null || !headers.containsKey(PROXY_AUTHORIZATION_HEADER));
118+
119+
// Verify NTLM properties are set correctly for the MINA proxy handler to use
120+
Map<String, String> props = request.getProperties();
121+
assertNotNull("Properties should not be null", props);
122+
assertTrue("Properties should contain user credentials", props.size() >= 4);
123+
}
124+
125+
@Test
126+
public void shouldNotSetAuthorizationHeaderWhenCredentialsNotProvided() throws ConfigError {
127+
InetSocketAddress address = new InetSocketAddress(AvailablePortFinder.getNextAvailable());
128+
InetSocketAddress proxyAddress = new InetSocketAddress(AvailablePortFinder.getNextAvailable());
129+
130+
IoConnector connector = ProtocolFactory.createIoConnector(address);
131+
ProxyConnector proxyConnector = ProtocolFactory
132+
.createIoProxyConnector((SocketConnector) connector, address, proxyAddress, "http", "1.0", null,
133+
null, null, null);
134+
135+
ProxyIoSession proxySession = proxyConnector.getProxyIoSession();
136+
137+
// Verify HttpSmartProxyHandler is set even when no credentials provided
138+
assertNotNull("Handler should be set", proxySession.getHandler());
139+
assertTrue("Handler should be HttpSmartProxyHandler when no credentials",
140+
proxySession.getHandler() instanceof HttpSmartProxyHandler);
141+
142+
HttpProxyRequest request = (HttpProxyRequest) proxySession.getRequest();
143+
144+
Map<String, List<String>> headers = request.getHeaders();
145+
// Headers should either be null or not contain Proxy-Authorization
146+
assertTrue("Headers should be null or empty when no credentials provided",
147+
headers == null || !headers.containsKey(PROXY_AUTHORIZATION_HEADER));
148+
}
49149
}

0 commit comments

Comments
 (0)