Skip to content

Commit c4f074f

Browse files
authored
Merge pull request #1136 from quickfix-j/copilot/add-unit-test-for-error-code-407
Add `Proxy-Authorization` header for HTTP proxy `Basic` authentication
2 parents 3dba033 + 05b9f0c commit c4f074f

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

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

Lines changed: 21 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;
@@ -52,6 +57,8 @@ public class ProtocolFactory {
5257

5358
public final static int SOCKET = 0;
5459
public final static int VM_PIPE = 1;
60+
61+
public final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
5562

5663
public static String getTypeString(int type) {
5764
switch (type) {
@@ -155,12 +162,26 @@ private static ProxyRequest createHttpProxyRequest(InetSocketAddress address,
155162

156163
HttpProxyRequest req = new HttpProxyRequest(address);
157164
req.setProperties(props);
165+
158166
if (proxyVersion != null && proxyVersion.equalsIgnoreCase("1.1")) {
159167
req.setHttpVersion(HttpProxyConstants.HTTP_1_1);
160168
} else {
161169
req.setHttpVersion(HttpProxyConstants.HTTP_1_0);
162170
}
163171

172+
// Set Proxy-Authorization header for Basic authentication if credentials are provided
173+
// Some proxy servers require this header to be set upfront rather than waiting for a 407 response
174+
// Note: NTLM authentication requires a multi-step handshake and should not set headers upfront
175+
if (proxyUser != null && !proxyUser.isEmpty()
176+
&& proxyPassword != null && !proxyPassword.isEmpty()
177+
&& proxyDomain == null && proxyWorkstation == null) {
178+
Map<String, List<String>> headers = new HashMap<>();
179+
String credentials = proxyUser + ":" + proxyPassword;
180+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
181+
headers.put(PROXY_AUTHORIZATION_HEADER, Collections.singletonList("Basic " + encodedCredentials));
182+
req.setHeaders(headers);
183+
}
184+
164185
return req;
165186
}
166187

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,23 @@
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;
2425
import org.apache.mina.proxy.session.ProxyIoSession;
2526
import org.apache.mina.transport.socket.SocketConnector;
2627
import org.apache.mina.util.AvailablePortFinder;
2728
import org.junit.Test;
2829
import quickfix.ConfigError;
2930

3031
import java.net.InetSocketAddress;
32+
import java.util.Base64;
33+
import java.util.List;
34+
import java.util.Map;
3135

36+
import static org.junit.Assert.assertEquals;
37+
import static org.junit.Assert.assertNotNull;
3238
import static org.junit.Assert.assertNull;
39+
import static org.junit.Assert.assertTrue;
40+
import static quickfix.mina.ProtocolFactory.PROXY_AUTHORIZATION_HEADER;
3341

3442
public class ProtocolFactoryTest {
3543

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

0 commit comments

Comments
 (0)