Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Digest auth support #10

Merged
merged 10 commits into from
Mar 10, 2019
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>com.microsoft.azure</groupId>
<artifactId>qpid-proton-j-extensions</artifactId>
<version>1.1.0</version>
<version>1.1.0-SNAPSHOT</version>

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public interface Proxy {
enum ProxyState {
PN_PROXY_NOT_STARTED,
PN_PROXY_CONNECTING,
PN_PROXY_CHALLENGE,
PN_PROXY_CHALLENGE_RESPONDED,
PN_PROXY_CONNECTED,
PN_PROXY_FAILED
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.microsoft.azure.proton.transport.proxy;

import java.util.Map;

public interface ProxyChallengeProcessor {
Map<String, String> getHeader();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.microsoft.azure.proton.transport.proxy.impl;

import com.microsoft.azure.proton.transport.proxy.ProxyChallengeProcessor;

import java.net.*;
import java.util.*;

public class BasicProxyChallengeProcessorImpl implements ProxyChallengeProcessor {

private final String BASIC = "basic";
private final ProxyAuthenticator proxyAuthenticator;
private final Map<String, String> headers;
private String host;

BasicProxyChallengeProcessorImpl(String host) {
this.host = host;
headers = new HashMap<>();
proxyAuthenticator = new ProxyAuthenticator();
}

@Override
public Map<String, String> getHeader() {
PasswordAuthentication passwordAuthentication = proxyAuthenticator.getPasswordAuthentication(BASIC, host);
if (!proxyAuthenticator.isPasswordAuthenticationHasValues(passwordAuthentication))
return null;

String proxyUserName = passwordAuthentication.getUserName();
String proxyPassword = new String(passwordAuthentication.getPassword());
final String usernamePasswordPair = proxyUserName + ":" + proxyPassword;

headers.put(
"Proxy-Authorization",
"Basic " + Base64.getEncoder().encodeToString(usernamePasswordPair.getBytes()));
return headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.microsoft.azure.proton.transport.proxy.impl;

import com.microsoft.azure.proton.transport.proxy.ProxyChallengeProcessor;

import javax.xml.bind.DatatypeConverter;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

public class DigestProxyChallengeProcessorImpl implements ProxyChallengeProcessor {

private final String DIGEST = "digest";
private final String PROXY_AUTH_DIGEST = "Proxy-Authenticate: Digest";
private final AtomicInteger nonceCounter = new AtomicInteger(0);
private final Map<String, String> headers;
private final ProxyAuthenticator proxyAuthenticator;

private static String host;
private static String challenge;

DigestProxyChallengeProcessorImpl(String host, String challenge) {
this.host = host;
this.challenge = challenge;
headers = new HashMap<>();
proxyAuthenticator = new ProxyAuthenticator();
}

@Override
public Map<String, String> getHeader() {
final Scanner responseScanner = new Scanner(challenge);
final Map<String, String> challengeQuestionValues = new HashMap<>();
while (responseScanner.hasNextLine()) {
String line = responseScanner.nextLine();
if (line.contains(PROXY_AUTH_DIGEST)){
getChallengeQuestionHeaders(line, challengeQuestionValues);
computeDigestAuthHeader(challengeQuestionValues, host, proxyAuthenticator.getPasswordAuthentication(DIGEST, host));
break;
}
}
return headers;
}

private void getChallengeQuestionHeaders(String line, Map<String, String> challengeQuestionValues) {
String context = line.substring(PROXY_AUTH_DIGEST.length());
String[] headerValues = context.split(",");

for (String headerValue : headerValues) {
if (headerValue.contains("=")) {
String key = headerValue.substring(0, headerValue.indexOf("="));
String value = headerValue.substring(headerValue.indexOf("=") + 1);
challengeQuestionValues.put(key.trim(), value.replaceAll("\"", "").trim());
}
}
}

private void computeDigestAuthHeader(Map<String, String> challengeQuestionValues,
String uri,
PasswordAuthentication passwordAuthentication) {
if (!proxyAuthenticator.isPasswordAuthenticationHasValues(passwordAuthentication))
return;

String proxyUserName = passwordAuthentication.getUserName();
String proxyPassword = new String(passwordAuthentication.getPassword());
String digestValue;
try {
String nonce = challengeQuestionValues.get("nonce");
String realm = challengeQuestionValues.get("realm");
String qop = challengeQuestionValues.get("qop");

MessageDigest md5 = MessageDigest.getInstance("md5");
SecureRandom secureRandom = new SecureRandom();
String a1 = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s:%s", proxyUserName, realm, proxyPassword).getBytes("UTF-8"))).toLowerCase();
String a2 = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s", "CONNECT", uri).getBytes("UTF-8"))).toLowerCase();

byte[] cnonceBytes = new byte[16];
secureRandom.nextBytes(cnonceBytes);
String cnonce = DatatypeConverter.printHexBinary(cnonceBytes).toLowerCase();
String response;
if (qop == null || qop.isEmpty()) {
response = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s:%s", a1, nonce, a2).getBytes("UTF-8"))).toLowerCase();
digestValue = String.format("Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",response=\"%s\"",
proxyUserName, realm, nonce, uri, cnonce, response);
} else {
int nc = nonceCounter.incrementAndGet();
response = DatatypeConverter.printHexBinary(md5.digest(String.format("%s:%s:%08X:%s:%s:%s", a1, nonce, nc, cnonce, qop, a2).getBytes("UTF-8"))).toLowerCase();
digestValue = String.format("Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",nc=%08X,response=\"%s\",qop=\"%s\"",
proxyUserName, realm, nonce, uri, cnonce, nc, response, qop);
}

headers.put("Proxy-Authorization", digestValue);
} catch(NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.microsoft.azure.proton.transport.proxy.impl;

import java.net.*;
import java.util.List;

public class ProxyAuthenticator {

public PasswordAuthentication getPasswordAuthentication(String scheme, String host) {
ProxySelector proxySelector = ProxySelector.getDefault();

URI uri;
List<Proxy> proxies = null;
if (host != null && !host.isEmpty()) {
uri = URI.create(host);
proxies = proxySelector.select(uri);
}

InetAddress proxyAddr = null;
java.net.Proxy.Type proxyType = null;
if (isProxyAddressLegal(proxies)) {
// will be only one element in the proxy list
proxyAddr = ((InetSocketAddress)proxies.get(0).address()).getAddress();
proxyType = proxies.get(0).type();
}
return Authenticator.requestPasswordAuthentication(
"",
proxyAddr,
0,
proxyType == null ? "" : proxyType.name(),
"Event Hubs client websocket proxy support",
scheme,
null,
Authenticator.RequestorType.PROXY);
}

public boolean isPasswordAuthenticationHasValues(PasswordAuthentication passwordAuthentication){
if (passwordAuthentication == null) return false;
String proxyUserName = passwordAuthentication.getUserName() != null
? passwordAuthentication.getUserName()
: null ;
String proxyPassword = passwordAuthentication.getPassword() != null
? new String(passwordAuthentication.getPassword())
: null;
if (isNullOrEmpty(proxyUserName) || isNullOrEmpty(proxyPassword)) return false;
return true;
}

private boolean isProxyAddressLegal(final List<Proxy> proxies) {
return proxies != null
&& !proxies.isEmpty()
&& proxies.get(0).address() != null
&& proxies.get(0).address() instanceof InetSocketAddress;
}

private boolean isNullOrEmpty(String string) {
return (string == null || string.isEmpty());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;

import java.nio.ByteBuffer;

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.qpid.proton.engine.Transport;
import org.apache.qpid.proton.engine.TransportException;
Expand All @@ -22,7 +24,7 @@
import org.apache.qpid.proton.engine.impl.TransportWrapper;

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

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

private ProxyHandler proxyHandler;

private final String PROXY_AUTH_DIGEST = "Proxy-Authenticate: Digest";
private final String PROXY_AUTH_BASIC = "Proxy-Authenticate: Basic";
private final AtomicInteger nonceCounter = new AtomicInteger(0);
/**
* Create proxy transport layer - which, after configuring using
* the {@link #configure(String, Map, ProxyHandler, Transport)} API
Expand Down Expand Up @@ -167,9 +172,17 @@ public void process() throws TransportException {
final ProxyHandler.ProxyResponseResult responseResult = proxyHandler
.validateProxyResponse(inputBuffer);
inputBuffer.compact();

inputBuffer.clear();
if (responseResult.getIsSuccess()) {
proxyState = ProxyState.PN_PROXY_CONNECTED;
} else if (responseResult.getError() != null && responseResult.getError().contains(PROXY_AUTH_DIGEST)) {
proxyState = ProxyState.PN_PROXY_CHALLENGE;
DigestProxyChallengeProcessorImpl digestProxyChallengeProcessor = new DigestProxyChallengeProcessorImpl(host, responseResult.getError());
headers = digestProxyChallengeProcessor.getHeader();
} else if (responseResult.getError() != null && responseResult.getError().contains(PROXY_AUTH_BASIC)) {
proxyState = ProxyState.PN_PROXY_CHALLENGE;
BasicProxyChallengeProcessorImpl basicProxyChallengeProcessor = new BasicProxyChallengeProcessorImpl(host);
headers = basicProxyChallengeProcessor.getHeader();
} else {
tailClosed = true;
underlyingTransport.closed(
Expand All @@ -178,6 +191,22 @@ public void process() throws TransportException {
+ responseResult.getError()));
}
break;
case PN_PROXY_CHALLENGE_RESPONDED:
inputBuffer.flip();
final ProxyHandler.ProxyResponseResult challengeResponseResult = proxyHandler
.validateProxyResponse(inputBuffer);
inputBuffer.compact();

if (challengeResponseResult.getIsSuccess()) {
proxyState = ProxyState.PN_PROXY_CONNECTED;
} else {
tailClosed = true;
underlyingTransport.closed(
new TransportException(
"proxy connect request failed with error: "
+ challengeResponseResult.getError()));
}
break;
default:
underlyingInput.process();
}
Expand All @@ -192,7 +221,6 @@ public void close_tail() {
if (getIsHandshakeInProgress()) {
headClosed = true;
}

underlyingInput.close_tail();
}

Expand All @@ -215,15 +243,35 @@ public int pending() {
} else {
return outputBuffer.position();
}
case PN_PROXY_CHALLENGE:
if (outputBuffer.position() == 0) {
proxyState = ProxyState.PN_PROXY_CHALLENGE_RESPONDED;
writeProxyRequest();

head.limit(outputBuffer.position());
if (headClosed) {
proxyState = ProxyState.PN_PROXY_FAILED;
return Transport.END_OF_STREAM;
} else {
return outputBuffer.position();
}
} else {
return outputBuffer.position();
}
case PN_PROXY_CHALLENGE_RESPONDED:
if (headClosed && (outputBuffer.position() == 0)) {
proxyState = ProxyState.PN_PROXY_FAILED;
return Transport.END_OF_STREAM;
} else {
return outputBuffer.position();
}
case PN_PROXY_CONNECTING:
if (headClosed && (outputBuffer.position() == 0)) {
proxyState = ProxyState.PN_PROXY_FAILED;
return Transport.END_OF_STREAM;
} else {
return outputBuffer.position();
}

default:
return Transport.END_OF_STREAM;
}
Expand All @@ -238,6 +286,8 @@ public ByteBuffer head() {
switch (proxyState) {
case PN_PROXY_CONNECTING:
return head;
case PN_PROXY_CHALLENGE_RESPONDED:
return head;
default:
return underlyingOutput.head();
}
Expand All @@ -261,6 +311,17 @@ public void pop(int bytes) {
underlyingOutput.pop(bytes);
}
break;
case PN_PROXY_CHALLENGE_RESPONDED:
if (outputBuffer.position() != 0) {
outputBuffer.flip();
outputBuffer.position(bytes);
outputBuffer.compact();
head.position(0);
head.limit(outputBuffer.position());
} else {
underlyingOutput.pop(bytes);
}
break;
default:
underlyingOutput.pop(bytes);
}
Expand All @@ -274,5 +335,6 @@ public void close_head() {
headClosed = true;
underlyingOutput.close_head();
}

}
}
Loading