Skip to content

Commit 3c28728

Browse files
Merge pull request #10 from mssfang/DigestAuthSupport
Current proxy implementation has a limitation: we do not negotiate the Auth mechanisms with the Proxy and assume that it is going to be No-Auth or Basic-Auth. With java 1.8's recent update - Basic Auth is no longer part of the defualt settings for jre applications. This PR adds the below 2 functionalities and brings us relevant to the current security standards: 1) adds proxy authentication negotiation 2) implements digest auth.
2 parents d6c2b71 + 59cdd87 commit 3c28728

File tree

10 files changed

+384
-6
lines changed

10 files changed

+384
-6
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>com.microsoft.azure</groupId>
99
<artifactId>qpid-proton-j-extensions</artifactId>
10-
<version>1.1.0</version>
10+
<version>1.1.0-SNAPSHOT</version>
1111

1212
<url>https://github.com/Azure/qpid-proton-j-extensions</url>
1313

src/main/java/com/microsoft/azure/proton/transport/proxy/Proxy.java

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public interface Proxy {
1313
enum ProxyState {
1414
PN_PROXY_NOT_STARTED,
1515
PN_PROXY_CONNECTING,
16+
PN_PROXY_CHALLENGE,
17+
PN_PROXY_CHALLENGE_RESPONDED,
1618
PN_PROXY_CONNECTED,
1719
PN_PROXY_FAILED
1820
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.microsoft.azure.proton.transport.proxy;
2+
3+
import java.util.Map;
4+
5+
public interface ProxyChallengeProcessor {
6+
Map<String, String> getHeader();
7+
}
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.microsoft.azure.proton.transport.proxy.impl;
2+
3+
import com.microsoft.azure.proton.transport.proxy.ProxyChallengeProcessor;
4+
5+
import java.net.*;
6+
import java.util.*;
7+
8+
public class BasicProxyChallengeProcessorImpl implements ProxyChallengeProcessor {
9+
10+
private final String BASIC = "basic";
11+
private final ProxyAuthenticator proxyAuthenticator;
12+
private final Map<String, String> headers;
13+
private String host;
14+
15+
BasicProxyChallengeProcessorImpl(String host) {
16+
this.host = host;
17+
headers = new HashMap<>();
18+
proxyAuthenticator = new ProxyAuthenticator();
19+
}
20+
21+
@Override
22+
public Map<String, String> getHeader() {
23+
PasswordAuthentication passwordAuthentication = proxyAuthenticator.getPasswordAuthentication(BASIC, host);
24+
if (!proxyAuthenticator.isPasswordAuthenticationHasValues(passwordAuthentication))
25+
return null;
26+
27+
String proxyUserName = passwordAuthentication.getUserName();
28+
String proxyPassword = new String(passwordAuthentication.getPassword());
29+
final String usernamePasswordPair = proxyUserName + ":" + proxyPassword;
30+
31+
headers.put(
32+
"Proxy-Authorization",
33+
"Basic " + Base64.getEncoder().encodeToString(usernamePasswordPair.getBytes()));
34+
return headers;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.microsoft.azure.proton.transport.proxy.impl;
2+
3+
import com.microsoft.azure.proton.transport.proxy.ProxyChallengeProcessor;
4+
5+
import javax.xml.bind.DatatypeConverter;
6+
import java.io.UnsupportedEncodingException;
7+
import java.net.*;
8+
import java.security.MessageDigest;
9+
import java.security.NoSuchAlgorithmException;
10+
import java.security.SecureRandom;
11+
import java.util.*;
12+
import java.util.concurrent.atomic.AtomicInteger;
13+
14+
public class DigestProxyChallengeProcessorImpl implements ProxyChallengeProcessor {
15+
16+
private final String DIGEST = "digest";
17+
private final String PROXY_AUTH_DIGEST = "Proxy-Authenticate: Digest";
18+
private final AtomicInteger nonceCounter = new AtomicInteger(0);
19+
private final Map<String, String> headers;
20+
private final ProxyAuthenticator proxyAuthenticator;
21+
22+
private static String host;
23+
private static String challenge;
24+
25+
DigestProxyChallengeProcessorImpl(String host, String challenge) {
26+
this.host = host;
27+
this.challenge = challenge;
28+
headers = new HashMap<>();
29+
proxyAuthenticator = new ProxyAuthenticator();
30+
}
31+
32+
@Override
33+
public Map<String, String> getHeader() {
34+
final Scanner responseScanner = new Scanner(challenge);
35+
final Map<String, String> challengeQuestionValues = new HashMap<>();
36+
while (responseScanner.hasNextLine()) {
37+
String line = responseScanner.nextLine();
38+
if (line.contains(PROXY_AUTH_DIGEST)){
39+
getChallengeQuestionHeaders(line, challengeQuestionValues);
40+
computeDigestAuthHeader(challengeQuestionValues, host, proxyAuthenticator.getPasswordAuthentication(DIGEST, host));
41+
break;
42+
}
43+
}
44+
return headers;
45+
}
46+
47+
private void getChallengeQuestionHeaders(String line, Map<String, String> challengeQuestionValues) {
48+
String context = line.substring(PROXY_AUTH_DIGEST.length());
49+
String[] headerValues = context.split(",");
50+
51+
for (String headerValue : headerValues) {
52+
if (headerValue.contains("=")) {
53+
String key = headerValue.substring(0, headerValue.indexOf("="));
54+
String value = headerValue.substring(headerValue.indexOf("=") + 1);
55+
challengeQuestionValues.put(key.trim(), value.replaceAll("\"", "").trim());
56+
}
57+
}
58+
}
59+
60+
private void computeDigestAuthHeader(Map<String, String> challengeQuestionValues,
61+
String uri,
62+
PasswordAuthentication passwordAuthentication) {
63+
if (!proxyAuthenticator.isPasswordAuthenticationHasValues(passwordAuthentication))
64+
return;
65+
66+
String proxyUserName = passwordAuthentication.getUserName();
67+
String proxyPassword = new String(passwordAuthentication.getPassword());
68+
String digestValue;
69+
try {
70+
String nonce = challengeQuestionValues.get("nonce");
71+
String realm = challengeQuestionValues.get("realm");
72+
String qop = challengeQuestionValues.get("qop");
73+
74+
MessageDigest md5 = MessageDigest.getInstance("md5");
75+
SecureRandom secureRandom = new SecureRandom();
76+
String a1 = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s:%s", proxyUserName, realm, proxyPassword).getBytes("UTF-8"))).toLowerCase();
77+
String a2 = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s", "CONNECT", uri).getBytes("UTF-8"))).toLowerCase();
78+
79+
byte[] cnonceBytes = new byte[16];
80+
secureRandom.nextBytes(cnonceBytes);
81+
String cnonce = DatatypeConverter.printHexBinary(cnonceBytes).toLowerCase();
82+
String response;
83+
if (qop == null || qop.isEmpty()) {
84+
response = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s:%s", a1, nonce, a2).getBytes("UTF-8"))).toLowerCase();
85+
digestValue = String.format("Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",response=\"%s\"",
86+
proxyUserName, realm, nonce, uri, cnonce, response);
87+
} else {
88+
int nc = nonceCounter.incrementAndGet();
89+
response = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s:%08X:%s:%s:%s", a1, nonce, nc, cnonce, qop, a2).getBytes("UTF-8"))).toLowerCase();
90+
digestValue = String.format("Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",nc=%08X,response=\"%s\",qop=\"%s\"",
91+
proxyUserName, realm, nonce, uri, cnonce, nc, response, qop);
92+
}
93+
94+
headers.put("Proxy-Authorization", digestValue);
95+
} catch(NoSuchAlgorithmException ex) {
96+
throw new RuntimeException(ex);
97+
} catch (UnsupportedEncodingException ex) {
98+
throw new RuntimeException(ex);
99+
}
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.microsoft.azure.proton.transport.proxy.impl;
2+
3+
import java.net.*;
4+
import java.util.List;
5+
6+
public class ProxyAuthenticator {
7+
8+
public PasswordAuthentication getPasswordAuthentication(String scheme, String host) {
9+
ProxySelector proxySelector = ProxySelector.getDefault();
10+
11+
URI uri;
12+
List<Proxy> proxies = null;
13+
if (host != null && !host.isEmpty()) {
14+
uri = URI.create(host);
15+
proxies = proxySelector.select(uri);
16+
}
17+
18+
InetAddress proxyAddr = null;
19+
java.net.Proxy.Type proxyType = null;
20+
if (isProxyAddressLegal(proxies)) {
21+
// will be only one element in the proxy list
22+
proxyAddr = ((InetSocketAddress)proxies.get(0).address()).getAddress();
23+
proxyType = proxies.get(0).type();
24+
}
25+
return Authenticator.requestPasswordAuthentication(
26+
"",
27+
proxyAddr,
28+
0,
29+
proxyType == null ? "" : proxyType.name(),
30+
"Event Hubs client websocket proxy support",
31+
scheme,
32+
null,
33+
Authenticator.RequestorType.PROXY);
34+
}
35+
36+
public boolean isPasswordAuthenticationHasValues(PasswordAuthentication passwordAuthentication){
37+
if (passwordAuthentication == null) return false;
38+
String proxyUserName = passwordAuthentication.getUserName() != null
39+
? passwordAuthentication.getUserName()
40+
: null ;
41+
String proxyPassword = passwordAuthentication.getPassword() != null
42+
? new String(passwordAuthentication.getPassword())
43+
: null;
44+
if (isNullOrEmpty(proxyUserName) || isNullOrEmpty(proxyPassword)) return false;
45+
return true;
46+
}
47+
48+
private boolean isProxyAddressLegal(final List<Proxy> proxies) {
49+
return proxies != null
50+
&& !proxies.isEmpty()
51+
&& proxies.get(0).address() != null
52+
&& proxies.get(0).address() instanceof InetSocketAddress;
53+
}
54+
55+
private boolean isNullOrEmpty(String string) {
56+
return (string == null || string.isEmpty());
57+
}
58+
59+
}

src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java

+66-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
1212

1313
import java.nio.ByteBuffer;
14+
1415
import java.util.Map;
16+
import java.util.concurrent.atomic.AtomicInteger;
1517

1618
import org.apache.qpid.proton.engine.Transport;
1719
import org.apache.qpid.proton.engine.TransportException;
@@ -22,7 +24,7 @@
2224
import org.apache.qpid.proton.engine.impl.TransportWrapper;
2325

2426
public class ProxyImpl implements Proxy, TransportLayer {
25-
private final int proxyHandshakeBufferSize = 4 * 1024; // buffers used only for proxy-handshake
27+
private final int proxyHandshakeBufferSize = 8 * 1024; // buffers used only for proxy-handshake
2628
private final ByteBuffer inputBuffer;
2729
private final ByteBuffer outputBuffer;
2830

@@ -36,6 +38,9 @@ public class ProxyImpl implements Proxy, TransportLayer {
3638

3739
private ProxyHandler proxyHandler;
3840

41+
private final String PROXY_AUTH_DIGEST = "Proxy-Authenticate: Digest";
42+
private final String PROXY_AUTH_BASIC = "Proxy-Authenticate: Basic";
43+
private final AtomicInteger nonceCounter = new AtomicInteger(0);
3944
/**
4045
* Create proxy transport layer - which, after configuring using
4146
* the {@link #configure(String, Map, ProxyHandler, Transport)} API
@@ -167,9 +172,17 @@ public void process() throws TransportException {
167172
final ProxyHandler.ProxyResponseResult responseResult = proxyHandler
168173
.validateProxyResponse(inputBuffer);
169174
inputBuffer.compact();
170-
175+
inputBuffer.clear();
171176
if (responseResult.getIsSuccess()) {
172177
proxyState = ProxyState.PN_PROXY_CONNECTED;
178+
} else if (responseResult.getError() != null && responseResult.getError().contains(PROXY_AUTH_DIGEST)) {
179+
proxyState = ProxyState.PN_PROXY_CHALLENGE;
180+
DigestProxyChallengeProcessorImpl digestProxyChallengeProcessor = new DigestProxyChallengeProcessorImpl(host, responseResult.getError());
181+
headers = digestProxyChallengeProcessor.getHeader();
182+
} else if (responseResult.getError() != null && responseResult.getError().contains(PROXY_AUTH_BASIC)) {
183+
proxyState = ProxyState.PN_PROXY_CHALLENGE;
184+
BasicProxyChallengeProcessorImpl basicProxyChallengeProcessor = new BasicProxyChallengeProcessorImpl(host);
185+
headers = basicProxyChallengeProcessor.getHeader();
173186
} else {
174187
tailClosed = true;
175188
underlyingTransport.closed(
@@ -178,6 +191,22 @@ public void process() throws TransportException {
178191
+ responseResult.getError()));
179192
}
180193
break;
194+
case PN_PROXY_CHALLENGE_RESPONDED:
195+
inputBuffer.flip();
196+
final ProxyHandler.ProxyResponseResult challengeResponseResult = proxyHandler
197+
.validateProxyResponse(inputBuffer);
198+
inputBuffer.compact();
199+
200+
if (challengeResponseResult.getIsSuccess()) {
201+
proxyState = ProxyState.PN_PROXY_CONNECTED;
202+
} else {
203+
tailClosed = true;
204+
underlyingTransport.closed(
205+
new TransportException(
206+
"proxy connect request failed with error: "
207+
+ challengeResponseResult.getError()));
208+
}
209+
break;
181210
default:
182211
underlyingInput.process();
183212
}
@@ -192,7 +221,6 @@ public void close_tail() {
192221
if (getIsHandshakeInProgress()) {
193222
headClosed = true;
194223
}
195-
196224
underlyingInput.close_tail();
197225
}
198226

@@ -215,15 +243,35 @@ public int pending() {
215243
} else {
216244
return outputBuffer.position();
217245
}
246+
case PN_PROXY_CHALLENGE:
247+
if (outputBuffer.position() == 0) {
248+
proxyState = ProxyState.PN_PROXY_CHALLENGE_RESPONDED;
249+
writeProxyRequest();
218250

251+
head.limit(outputBuffer.position());
252+
if (headClosed) {
253+
proxyState = ProxyState.PN_PROXY_FAILED;
254+
return Transport.END_OF_STREAM;
255+
} else {
256+
return outputBuffer.position();
257+
}
258+
} else {
259+
return outputBuffer.position();
260+
}
261+
case PN_PROXY_CHALLENGE_RESPONDED:
262+
if (headClosed && (outputBuffer.position() == 0)) {
263+
proxyState = ProxyState.PN_PROXY_FAILED;
264+
return Transport.END_OF_STREAM;
265+
} else {
266+
return outputBuffer.position();
267+
}
219268
case PN_PROXY_CONNECTING:
220269
if (headClosed && (outputBuffer.position() == 0)) {
221270
proxyState = ProxyState.PN_PROXY_FAILED;
222271
return Transport.END_OF_STREAM;
223272
} else {
224273
return outputBuffer.position();
225274
}
226-
227275
default:
228276
return Transport.END_OF_STREAM;
229277
}
@@ -238,6 +286,8 @@ public ByteBuffer head() {
238286
switch (proxyState) {
239287
case PN_PROXY_CONNECTING:
240288
return head;
289+
case PN_PROXY_CHALLENGE_RESPONDED:
290+
return head;
241291
default:
242292
return underlyingOutput.head();
243293
}
@@ -261,6 +311,17 @@ public void pop(int bytes) {
261311
underlyingOutput.pop(bytes);
262312
}
263313
break;
314+
case PN_PROXY_CHALLENGE_RESPONDED:
315+
if (outputBuffer.position() != 0) {
316+
outputBuffer.flip();
317+
outputBuffer.position(bytes);
318+
outputBuffer.compact();
319+
head.position(0);
320+
head.limit(outputBuffer.position());
321+
} else {
322+
underlyingOutput.pop(bytes);
323+
}
324+
break;
264325
default:
265326
underlyingOutput.pop(bytes);
266327
}
@@ -274,5 +335,6 @@ public void close_head() {
274335
headClosed = true;
275336
underlyingOutput.close_head();
276337
}
338+
277339
}
278340
}

0 commit comments

Comments
 (0)