diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/HttpStatusLine.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/HttpStatusLine.java
new file mode 100644
index 0000000..c8ac92b
--- /dev/null
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/HttpStatusLine.java
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * The first line in an HTTP 1.0/1.1 response. Consists of the HTTP protocol version, status code, and a reason phrase
+ * for the HTTP response.
+ *
+ * @see RFC 2616
+ */
+public final class HttpStatusLine {
+ private final String httpVersion;
+ private final int statusCode;
+ private final String reason;
+
+ /**
+ * Creates a new instance of {@link HttpStatusLine}.
+ *
+ * @param protocolVersion The HTTP protocol version. For example, 1.0, 1.1.
+ * @param statusCode A numeric status code for the HTTP response.
+ * @param reason Textual phrase representing the HTTP status code.
+ */
+ private HttpStatusLine(String protocolVersion, int statusCode, String reason) {
+ this.httpVersion = Objects.requireNonNull(protocolVersion, "'httpVersion' cannot be null.");
+ this.statusCode = statusCode;
+ this.reason = Objects.requireNonNull(reason, "'reason' cannot be null.");
+ }
+
+ /**
+ * Parses the provided {@code statusLine} into an HTTP status line.
+ *
+ * @param line Line to parse into an HTTP status line.
+ * @return A new instance of {@link HttpStatusLine} representing the given {@code statusLine}.
+ * @throws IllegalArgumentException if {@code line} is not the correct format of an HTTP status line. If it
+ * does not have a protocol version, status code, or reason component. Or, if the HTTP protocol version
+ * cannot be parsed.
+ */
+ public static HttpStatusLine create(String line) {
+ final String[] components = line.split(" ", 3);
+ if (components.length != 3) {
+ throw new IllegalArgumentException(String.format(Locale.ROOT,
+ "HTTP status-line is invalid. Line: %s", line));
+ }
+
+ final String[] protocol = components[0].split("/", 2);
+ if (protocol.length != 2) {
+ throw new IllegalArgumentException(String.format(Locale.ROOT,
+ "Protocol is invalid, expected HTTP/{version}. Actual: %s", components[0]));
+ }
+
+ int statusCode;
+ try {
+ statusCode = Integer.parseInt(components[1]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "HTTP Status code '%s' is not valid.", components[1]), e);
+ }
+
+ return new HttpStatusLine(protocol[1], statusCode, components[2]);
+ }
+
+ /**
+ * Gets the HTTP protocol version.
+ *
+ * @return The HTTP protocol version.
+ */
+ public String getProtocolVersion() {
+ return this.httpVersion;
+ }
+
+ /**
+ * Gets the HTTP status code.
+ *
+ * @return The HTTP status code.
+ */
+ public int getStatusCode() {
+ return this.statusCode;
+ }
+
+ /**
+ * Gets the textual representation for the HTTP status code.
+ *
+ * @return The textual representation for the HTTP status code.
+ */
+ public String getReason() {
+ return this.reason;
+ }
+}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyHandler.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyHandler.java
index f51f608..09a58b2 100644
--- a/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyHandler.java
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyHandler.java
@@ -3,7 +3,6 @@
package com.microsoft.azure.proton.transport.proxy;
-import java.nio.ByteBuffer;
import java.util.Map;
/**
@@ -11,33 +10,6 @@ import java.util.Map;
*/
public interface ProxyHandler {
- /**
- * Represents a response from the proxy.
- */
- class ProxyResponseResult {
- private final Boolean isSuccess;
- private final String error;
-
- /**
- * Creates a new response.
- *
- * @param isSuccess {@code true} if it was successful; {@code false} otherwise.
- * @param error The error from the proxy. Or {@code null} if there was none.
- */
- public ProxyResponseResult(final Boolean isSuccess, final String error) {
- this.isSuccess = isSuccess;
- this.error = error;
- }
-
- public Boolean getIsSuccess() {
- return isSuccess;
- }
-
- public String getError() {
- return error;
- }
- }
-
/**
* Creates a CONNECT request to the provided {@code hostName} and adds {@code additionalHeaders} to the request.
*
@@ -48,11 +20,11 @@ public interface ProxyHandler {
String createProxyRequest(String hostName, Map additionalHeaders);
/**
- * Verifies that {@code buffer} contains a successful CONNECT response.
+ * Verifies that {@code httpResponse} contains a successful CONNECT response.
+ *
+ * @param httpResponse HTTP response to validate for a successful CONNECT response.
+ * @return {@code true} if the HTTP response is successful and correct, and {@code false} otherwise.
*
- * @param buffer Buffer containing the HTTP response.
- * @return Indicates if CONNECT response contained a success. If not, contains an error indicating why the call was
- * not successful.
*/
- ProxyResponseResult validateProxyResponse(ByteBuffer buffer);
+ boolean validateProxyResponse(ProxyResponse httpResponse);
}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyResponse.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyResponse.java
new file mode 100644
index 0000000..57c9015
--- /dev/null
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyResponse.java
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents an HTTP response from a proxy.
+ */
+public interface ProxyResponse {
+ /**
+ * Gets the headers for the HTTP response.
+ *
+ * @return The headers for the HTTP response.
+ */
+ Map> getHeaders();
+
+ /**
+ * Gets the HTTP status line.
+ *
+ * @return The HTTP status line.
+ */
+ HttpStatusLine getStatus();
+
+ /**
+ * Gets the HTTP response body.
+ *
+ * @return The HTTP response body.
+ */
+ ByteBuffer getContents();
+
+ /**
+ * Gets the HTTP response body as an error.
+ *
+ * @return If there is no HTTP response body, an empty string is returned.
+ */
+ String getError();
+
+ /**
+ * Gets whether or not the HTTP response is complete. An HTTP response is complete when the HTTP header and body are
+ * received.
+ *
+ * @return {@code true} if the HTTP response is complete, and {@code false} otherwise.
+ */
+ boolean isMissingContent();
+
+ /**
+ * Adds contents to the body if it is missing content.
+ *
+ * @param contents Contents to add to the HTTP body.
+ */
+ void addContent(ByteBuffer contents);
+}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/Constants.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/Constants.java
index 28826a6..38fc662 100644
--- a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/Constants.java
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/Constants.java
@@ -9,7 +9,7 @@ import java.util.Locale;
* Package private constants.
*/
class Constants {
- static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate:";
+ static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
static final String DIGEST = "Digest";
@@ -18,4 +18,11 @@ class Constants {
static final String DIGEST_LOWERCASE = Constants.DIGEST.toLowerCase(Locale.ROOT);
static final String CONNECT = "CONNECT";
+
+ static final String PROXY_CONNECT_FAILED = "Proxy connect request failed with error: ";
+ static final String PROXY_CONNECT_USER_ERROR = "User configuration error. Using non-matching proxy authentication.";
+
+ static final int PROXY_HANDSHAKE_BUFFER_SIZE = 4 * 1024; // buffers used only for proxy-handshake
+
+ static final String CONTENT_LENGTH = "Content-Length";
}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImpl.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImpl.java
index e8736a7..96def56 100644
--- a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImpl.java
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImpl.java
@@ -28,7 +28,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/
public class DigestProxyChallengeProcessorImpl implements ProxyChallengeProcessor {
static final String DEFAULT_ALGORITHM = "MD5";
- private static final String PROXY_AUTH_DIGEST = Constants.PROXY_AUTHENTICATE_HEADER + " " + Constants.DIGEST;
+ private static final String PROXY_AUTH_DIGEST = Constants.DIGEST;
private static final char[] HEX_CODE = "0123456789ABCDEF".toCharArray();
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImpl.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImpl.java
index dd4bef1..3b2172c 100644
--- a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImpl.java
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImpl.java
@@ -3,16 +3,19 @@
package com.microsoft.azure.proton.transport.proxy.impl;
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
import com.microsoft.azure.proton.transport.proxy.Proxy;
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
-import java.util.Scanner;
-import java.util.function.Predicate;
-import java.util.regex.Pattern;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Implementation class that handles connecting to the proxy.
@@ -27,8 +30,10 @@ public class ProxyHandlerImpl implements ProxyHandler {
static final String CONNECT_REQUEST = "CONNECT %1$s HTTP/1.1%2$sHost: %1$s%2$sConnection: Keep-Alive%2$s";
static final String HEADER_FORMAT = "%s: %s";
static final String NEW_LINE = "\r\n";
- private final Pattern successStatusLine = Pattern.compile("^http/1\\.(0|1) (?2[0-9]{2})", Pattern.CASE_INSENSITIVE);
- private final Predicate successStatusLinePredicate = successStatusLine.asPredicate();
+
+ private static final String CONNECTION_ESTABLISHED = "connection established";
+ private static final Set SUPPORTED_VERSIONS = Stream.of("1.1", "1.0").collect(Collectors.toSet());
+ private final Logger logger = LoggerFactory.getLogger(ProxyHandlerImpl.class);
/**
* {@inheritDoc}
@@ -54,23 +59,17 @@ public class ProxyHandlerImpl implements ProxyHandler {
* {@inheritDoc}
*/
@Override
- public ProxyResponseResult validateProxyResponse(ByteBuffer buffer) {
- int size = buffer.remaining();
- String response = null;
+ public boolean validateProxyResponse(ProxyResponse response) {
+ Objects.requireNonNull(response, "'response' cannot be null.");
- if (size > 0) {
- byte[] responseBytes = new byte[buffer.remaining()];
- buffer.get(responseBytes);
- response = new String(responseBytes, StandardCharsets.UTF_8);
- final Scanner responseScanner = new Scanner(response);
- if (responseScanner.hasNextLine()) {
- final String firstLine = responseScanner.nextLine();
- if (successStatusLinePredicate.test(firstLine)) {
- return new ProxyResponseResult(true, null);
- }
- }
+ final HttpStatusLine status = response.getStatus();
+ if (status == null) {
+ logger.error("Response does not contain a status line. {}", response);
+ return false;
}
- return new ProxyResponseResult(false, response);
+ return status.getStatusCode() == 200
+ && SUPPORTED_VERSIONS.contains(status.getProtocolVersion())
+ && CONNECTION_ESTABLISHED.equalsIgnoreCase(status.getReason());
}
}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java
index eedaa6f..bd19ec3 100644
--- a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java
@@ -8,6 +8,7 @@ import com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType;
import com.microsoft.azure.proton.transport.proxy.ProxyChallengeProcessor;
import com.microsoft.azure.proton.transport.proxy.ProxyConfiguration;
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
import org.apache.qpid.proton.engine.Transport;
import org.apache.qpid.proton.engine.TransportException;
import org.apache.qpid.proton.engine.impl.TransportImpl;
@@ -19,16 +20,23 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Scanner;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType.BASIC;
import static com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType.DIGEST;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHENTICATE;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_CONNECT_FAILED;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_CONNECT_USER_ERROR;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_HANDSHAKE_BUFFER_SIZE;
import static org.apache.qpid.proton.engine.impl.ByteBufferUtils.newWriteableBuffer;
/**
@@ -40,9 +48,6 @@ import static org.apache.qpid.proton.engine.impl.ByteBufferUtils.newWriteableBuf
*/
public class ProxyImpl implements Proxy, TransportLayer {
private static final Logger LOGGER = LoggerFactory.getLogger(ProxyImpl.class);
- private static final String PROXY_CONNECT_FAILED = "Proxy connect request failed with error: ";
- private static final String PROXY_CONNECT_USER_ERROR = "User configuration error. Using non-matching proxy authentication.";
- private static final int PROXY_HANDSHAKE_BUFFER_SIZE = 8 * 1024; // buffers used only for proxy-handshake
private final ByteBuffer inputBuffer;
private final ByteBuffer outputBuffer;
@@ -50,28 +55,27 @@ public class ProxyImpl implements Proxy, TransportLayer {
private boolean tailClosed = false;
private boolean headClosed = false;
- private boolean isProxyConfigured;
private String host = "";
private Map headers = null;
private TransportImpl underlyingTransport;
- private ProxyState proxyState = ProxyState.PN_PROXY_NOT_STARTED;
private ProxyHandler proxyHandler;
+ private volatile boolean isProxyConfigured;
+ private volatile ProxyState proxyState = ProxyState.PN_PROXY_NOT_STARTED;
+
/**
- * Create proxy transport layer - which, after configuring using
- * the {@link #configure(String, Map, ProxyHandler, Transport)} API
- * is ready for layering in qpid-proton-j transport layers, using
- * {@link org.apache.qpid.proton.engine.impl.TransportInternal#addTransportLayer(TransportLayer)} API.
+ * Create proxy transport layer - which, after configuring using the {@link #configure(String, Map, ProxyHandler,
+ * Transport)} API is ready for layering in qpid-proton-j transport layers, using {@link
+ * org.apache.qpid.proton.engine.impl.TransportInternal#addTransportLayer(TransportLayer)} API.
*/
public ProxyImpl() {
this(null);
}
/**
- * Create proxy transport layer - which, after configuring using
- * the {@link #configure(String, Map, ProxyHandler, Transport)} API
- * is ready for layering in qpid-proton-j transport layers, using
- * {@link org.apache.qpid.proton.engine.impl.TransportInternal#addTransportLayer(TransportLayer)} API.
+ * Create proxy transport layer - which, after configuring using the {@link #configure(String, Map, ProxyHandler,
+ * Transport)} API is ready for layering in qpid-proton-j transport layers, using {@link
+ * org.apache.qpid.proton.engine.impl.TransportInternal#addTransportLayer(TransportLayer)} API.
*
* @param configuration Proxy configuration to use.
*/
@@ -133,7 +137,7 @@ public class ProxyImpl implements Proxy, TransportLayer {
return this.outputBuffer;
}
- protected Boolean getIsProxyConfigured() {
+ protected boolean getIsProxyConfigured() {
return this.isProxyConfigured;
}
@@ -180,10 +184,17 @@ public class ProxyImpl implements Proxy, TransportLayer {
private final TransportOutput underlyingOutput;
private final ByteBuffer head;
+ // Represents a response from a CONNECT request.
+ private final AtomicReference proxyResponse = new AtomicReference<>();
+
+ /**
+ * Creates a transport wrapper that wraps the WebSocket transport input and output.
+ */
ProxyTransportWrapper(TransportInput input, TransportOutput output) {
underlyingInput = input;
underlyingOutput = output;
head = outputBuffer.asReadOnlyBuffer();
+ head.limit(0);
}
@Override
@@ -231,62 +242,78 @@ public class ProxyImpl implements Proxy, TransportLayer {
switch (proxyState) {
case PN_PROXY_CONNECTING:
inputBuffer.flip();
- final ProxyHandler.ProxyResponseResult responseResult = proxyHandler.validateProxyResponse(inputBuffer);
- inputBuffer.compact();
- inputBuffer.clear();
+ final ProxyResponse connectResponse = readProxyResponse(inputBuffer);
+
+ if (connectResponse == null || connectResponse.isMissingContent()) {
+ LOGGER.info("Request is missing content. Waiting for more bytes.");
+ break;
+ }
+ //Clean up response to prepare for challenge
+ proxyResponse.set(null);
+
+ final boolean isSuccess = proxyHandler.validateProxyResponse(connectResponse);
// When connecting to proxy, it does not challenge us for authentication. If the user has specified
- // a configuration and it is not NONE, then we fail due to misconfiguration.
- if (responseResult.getIsSuccess()) {
+ // a configuration, and it is not NONE, then we fail due to misconfiguration.
+ if (isSuccess) {
if (proxyConfiguration == null || proxyConfiguration.authentication() == ProxyAuthenticationType.NONE) {
proxyState = ProxyState.PN_PROXY_CONNECTED;
} else {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("ProxyConfiguration mismatch. User configured: '{}', but authentication is not required",
- proxyConfiguration.authentication());
+ proxyConfiguration.authentication());
}
-
closeTailProxyError(PROXY_CONNECT_USER_ERROR);
}
break;
}
- final String challenge = responseResult.getError();
- final Set supportedTypes = getAuthenticationTypes(challenge);
+ final Map> headers = connectResponse.getHeaders();
+ final Set supportedTypes = getAuthenticationTypes(headers);
// The proxy did not successfully connect, user has specified that they want a particular
// authentication method, but it is not in list of supported authentication methods.
if (proxyConfiguration != null && !supportedTypes.contains(proxyConfiguration.authentication())) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("Proxy authentication required. User configured: '{}', but supported proxy authentication methods are: {}",
- proxyConfiguration.authentication(),
- supportedTypes.stream().map(type -> type.toString()).collect(Collectors.joining(",")));
+ proxyConfiguration.authentication(),
+ supportedTypes.stream().map(type -> type.toString()).collect(Collectors.joining(",")));
}
-
- closeTailProxyError(PROXY_CONNECT_USER_ERROR + PROXY_CONNECT_FAILED + challenge);
+ closeTailProxyError(PROXY_CONNECT_USER_ERROR + PROXY_CONNECT_FAILED
+ + connectResponse);
break;
}
+ final List challenges = headers.getOrDefault(PROXY_AUTHENTICATE, new ArrayList<>());
final ProxyChallengeProcessor processor = proxyConfiguration != null
- ? getChallengeProcessor(host, challenge, proxyConfiguration.authentication())
- : getChallengeProcessor(host, challenge, supportedTypes);
+ ? getChallengeProcessor(host, challenges, proxyConfiguration.authentication())
+ : getChallengeProcessor(host, challenges, supportedTypes);
if (processor != null) {
proxyState = ProxyState.PN_PROXY_CHALLENGE;
- headers = processor.getHeader();
+ ProxyImpl.this.headers = processor.getHeader();
} else {
- closeTailProxyError(PROXY_CONNECT_FAILED + challenge);
+ LOGGER.warn("Could not get ProxyChallengeProcessor for challenges.");
+ closeTailProxyError(PROXY_CONNECT_FAILED + String.join(";", challenges));
}
break;
case PN_PROXY_CHALLENGE_RESPONDED:
inputBuffer.flip();
- final ProxyHandler.ProxyResponseResult result = proxyHandler.validateProxyResponse(inputBuffer);
- inputBuffer.compact();
+ final ProxyResponse challengeResponse = readProxyResponse(inputBuffer);
- if (result.getIsSuccess()) {
+ if (challengeResponse == null || challengeResponse.isMissingContent()) {
+ LOGGER.warn("Request is missing content. Waiting for more bytes.");
+ break;
+ }
+ //Clean up
+ proxyResponse.set(null);
+
+ final boolean result = proxyHandler.validateProxyResponse(challengeResponse);
+
+ if (result) {
proxyState = ProxyState.PN_PROXY_CONNECTED;
} else {
- closeTailProxyError(PROXY_CONNECT_FAILED + result.getError());
+ closeTailProxyError(PROXY_CONNECT_FAILED + challengeResponse);
}
break;
default:
@@ -353,6 +380,11 @@ public class ProxyImpl implements Proxy, TransportLayer {
}
}
+ /**
+ * Gets the beginning of the output buffer.
+ *
+ * @return The beginning of the byte buffer.
+ */
@Override
public ByteBuffer head() {
if (getIsHandshakeInProgress()) {
@@ -368,6 +400,11 @@ public class ProxyImpl implements Proxy, TransportLayer {
}
}
+ /**
+ * Removes the first number of bytes from the output buffer.
+ *
+ * @param bytes The number of bytes to remove from the output buffer.
+ */
@Override
public void pop(int bytes) {
if (getIsHandshakeInProgress()) {
@@ -392,6 +429,9 @@ public class ProxyImpl implements Proxy, TransportLayer {
}
}
+ /**
+ * Closes the output transport.
+ */
@Override
public void close_head() {
headClosed = true;
@@ -402,18 +442,21 @@ public class ProxyImpl implements Proxy, TransportLayer {
* Gets the ProxyChallengeProcessor based on authentication types supported. Prefers DIGEST authentication if
* supported over BASIC. Returns null if it cannot match any supported types.
*/
- private ProxyChallengeProcessor getChallengeProcessor(String host, String challenge,
+ private ProxyChallengeProcessor getChallengeProcessor(String host, List challenges,
Set authentication) {
+ final ProxyAuthenticationType authType;
if (authentication.contains(DIGEST)) {
- return getChallengeProcessor(host, challenge, DIGEST);
+ authType = DIGEST;
} else if (authentication.contains(BASIC)) {
- return getChallengeProcessor(host, challenge, BASIC);
+ authType = BASIC;
} else {
return null;
}
+
+ return getChallengeProcessor(host, challenges, authType);
}
- private ProxyChallengeProcessor getChallengeProcessor(String host, String challenge,
+ private ProxyChallengeProcessor getChallengeProcessor(String host, List challenges,
ProxyAuthenticationType authentication) {
final ProxyAuthenticator authenticator = proxyConfiguration != null
? new ProxyAuthenticator(proxyConfiguration)
@@ -421,47 +464,45 @@ public class ProxyImpl implements Proxy, TransportLayer {
switch (authentication) {
case DIGEST:
- return new DigestProxyChallengeProcessorImpl(host, challenge, authenticator);
+ final Optional matching = challenges.stream()
+ .filter(challenge -> challenge.toLowerCase(Locale.ROOT).startsWith(Constants.DIGEST_LOWERCASE))
+ .findFirst();
+
+ return matching.map(c -> new DigestProxyChallengeProcessorImpl(host, c, authenticator))
+ .orElse(null);
case BASIC:
return new BasicProxyChallengeProcessorImpl(host, authenticator);
default:
+ LOGGER.warn("Authentication type does not have a challenge processor: {}", authentication);
return null;
}
}
/**
- * Gets the supported authentication types based on the {@code error}.
+ * Gets the supported authentication types based on the {@code headers}.
*
- * @param error Response from service call.
- * @return The supported proxy authentication methods. Or, an empty array if the value of {@code error} is
- * {@code null}, an empty string. Also, if it does not contain {@link Constants#PROXY_AUTHENTICATE_HEADER} with
- * {@link Constants#BASIC_LOWERCASE} or {@link Constants#DIGEST_LOWERCASE}.
+ * @param headers HTTP proxy response headers from service call.
+ * @return The supported proxy authentication methods. Or, an empty set if the value of {@code error} is {@code
+ * null}, an empty string. Or, if it does not contain{@link Constants#PROXY_AUTHENTICATE} with
+ * {@link Constants#BASIC_LOWERCASE} or {@link Constants#DIGEST_LOWERCASE}.
*/
- private Set getAuthenticationTypes(String error) {
- int index = error.indexOf(Constants.PROXY_AUTHENTICATE_HEADER);
-
- if (index == -1) {
+ private Set getAuthenticationTypes(Map> headers) {
+ if (!headers.containsKey(PROXY_AUTHENTICATE)) {
return Collections.emptySet();
}
- Set supportedTypes = new HashSet<>();
+ final Set supportedTypes = new HashSet<>();
+ final List authenticationTypes = headers.get(PROXY_AUTHENTICATE);
- try (Scanner scanner = new Scanner(error)) {
- while (scanner.hasNextLine()) {
- String line = scanner.nextLine().trim();
+ for (String type : authenticationTypes) {
+ final String lowercase = type.toLowerCase(Locale.ROOT);
- if (!line.startsWith(Constants.PROXY_AUTHENTICATE_HEADER)) {
- continue;
- }
-
- String substring = line.substring(Constants.PROXY_AUTHENTICATE_HEADER.length())
- .trim().toLowerCase(Locale.ROOT);
-
- if (substring.startsWith(Constants.BASIC_LOWERCASE)) {
- supportedTypes.add(BASIC);
- } else if (substring.startsWith(Constants.DIGEST_LOWERCASE)) {
- supportedTypes.add(DIGEST);
- }
+ if (lowercase.startsWith(Constants.BASIC_LOWERCASE)) {
+ supportedTypes.add(BASIC);
+ } else if (lowercase.startsWith(Constants.DIGEST_LOWERCASE)) {
+ supportedTypes.add(DIGEST);
+ } else {
+ LOGGER.warn("Did not understand this authentication type: {}", type);
}
}
@@ -472,5 +513,31 @@ public class ProxyImpl implements Proxy, TransportLayer {
tailClosed = true;
underlyingTransport.closed(new TransportException(errorMessage));
}
+
+ /**
+ * Given a byte buffer, reads a HTTP proxy response from it.
+ *
+ * @param buffer The buffer to read HTTP proxy response from.
+ * @return The current HTTP proxy response. Or {@code null} if one could not be read from the buffer and there
+ * is no current HTTP response.
+ */
+ private ProxyResponse readProxyResponse(ByteBuffer buffer) {
+ int size = buffer.remaining();
+ if (size <= 0) {
+ LOGGER.warn("InputBuffer is empty. Not reading any contents from it. Returning current response.");
+ return proxyResponse.get();
+ }
+
+ ProxyResponse current = proxyResponse.get();
+ if (current == null) {
+ proxyResponse.set(ProxyResponseImpl.create(buffer));
+ } else {
+ current.addContent(buffer);
+ }
+
+ buffer.compact();
+
+ return proxyResponse.get();
+ }
}
}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImpl.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImpl.java
new file mode 100644
index 0000000..534ec7f
--- /dev/null
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImpl.java
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy.impl;
+
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
+import com.microsoft.azure.proton.transport.ws.WebSocket.WebSocketFrameReadState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.CONTENT_LENGTH;
+
+/**
+ * Represents an HTTP response from a proxy.
+ *
+ * @see RFC2616
+ */
+public final class ProxyResponseImpl implements ProxyResponse {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ProxyResponseImpl.class);
+
+ private final HttpStatusLine status;
+ private final Map> headers;
+ private final ByteBuffer contents;
+
+ private ProxyResponseImpl(HttpStatusLine status, Map> headers, ByteBuffer contents) {
+ this.status = status;
+ this.headers = headers;
+ this.contents = contents;
+ }
+
+ /**
+ * Create a proxy response from a given {@code buffer}. Assumes that the {@code buffer} has been flipped.
+ *
+ * @param buffer Buffer which could parse to a proxy response.
+ * @return A new instance of {@link ProxyResponseImpl} representing the given buffer.
+ * @throws IllegalArgumentException if {@code buffer} have no content to read.
+ */
+ public static ProxyResponse create(ByteBuffer buffer) {
+ // Because we've flipped the buffer, position = 0, and the limit = size of the content.
+ int size = buffer.remaining();
+
+ if (size <= 0) {
+ throw new IllegalArgumentException(String.format("Cannot create response with buffer have no content. "
+ + "Limit: %s. Position: %s. Cap: %s", buffer.limit(), buffer.position(), buffer.capacity()));
+ }
+
+ final byte[] responseBytes = new byte[size];
+ buffer.get(responseBytes);
+
+ final String response = new String(responseBytes, StandardCharsets.UTF_8);
+ final String[] lines = response.split(StringUtils.NEW_LINE);
+ final Map> headers = new HashMap<>();
+
+ WebSocketFrameReadState frameReadState = WebSocketFrameReadState.INIT_READ;
+ HttpStatusLine statusLine = null;
+ ByteBuffer contents = ByteBuffer.allocate(0);
+
+ //Assume the full header message is in the first frame
+ for (String line : lines) {
+ switch (frameReadState) {
+ case INIT_READ:
+ statusLine = HttpStatusLine.create(line);
+ frameReadState = WebSocketFrameReadState.CHUNK_READ;
+ break;
+ case CHUNK_READ:
+ if (StringUtils.isNullOrEmpty(line)) {
+ // Now that we're done reading all the headers, figure out the size of the HTTP body and
+ // allocate an array of that size.
+ int length = 0;
+ if (headers.containsKey(CONTENT_LENGTH)) {
+ final List contentLength = headers.get(CONTENT_LENGTH);
+ length = Integer.parseInt(contentLength.get(0));
+ }
+
+ boolean hasBody = length > 0;
+ if (!hasBody) {
+ LOGGER.info("There is no content in the response. Response: {}", response);
+ return new ProxyResponseImpl(statusLine, headers, contents);
+ }
+
+ contents = ByteBuffer.allocate(length);
+ frameReadState = WebSocketFrameReadState.HEADER_READ;
+ } else {
+ final Map.Entry header = parseHeader(line);
+ final List value = headers.getOrDefault(header.getKey(), new ArrayList<>());
+
+ value.add(header.getValue());
+ headers.put(header.getKey(), value);
+ }
+ break;
+ case HEADER_READ:
+ if (contents.position() == 0) {
+ frameReadState = WebSocketFrameReadState.CONTINUED_FRAME_READ;
+ }
+
+ contents.put(line.getBytes(StandardCharsets.UTF_8));
+ contents.mark();
+ break;
+ case CONTINUED_FRAME_READ:
+ contents.put(line.getBytes(StandardCharsets.UTF_8));
+ contents.mark();
+ break;
+ default:
+ LOGGER.error("Unknown state: {}. Response: {}", frameReadState, response);
+ frameReadState = WebSocketFrameReadState.READ_ERROR;
+ break;
+ }
+ }
+
+
+ return new ProxyResponseImpl(statusLine, headers, contents);
+ }
+
+ private static Map.Entry parseHeader(String contents) {
+ final String[] split = contents.split(":", 2);
+
+ if (split.length != 2) {
+ throw new IllegalStateException("Line is not a valid header. Contents: " + contents);
+ }
+
+ return new AbstractMap.SimpleEntry<>(split[0].trim(), split[1].trim());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public HttpStatusLine getStatus() {
+ return status;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Map> getHeaders() {
+ return headers;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public ByteBuffer getContents() {
+ return contents.duplicate();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getError() {
+ final ByteBuffer readonly = contents.asReadOnlyBuffer();
+ readonly.flip();
+ return StandardCharsets.UTF_8.decode(readonly).toString();
+ }
+
+ /**
+ * Gets whether or not the HTTP response is complete. An HTTP response is complete when the HTTP header and body are
+ * received.
+ *
+ * @return {@code true} if the HTTP response is complete, and {@code false} otherwise.
+ */
+ public boolean isMissingContent() {
+ return contents.hasRemaining();
+ }
+
+ /**
+ * Adds additional content to the HTTP response's body. Assumes that the {@code content} has been flipped.
+ *
+ * @param content Content to add to the body of the HTTP response.
+ * @throws NullPointerException if {@code content} is {@code null}.
+ * @throws IllegalArgumentException if {@code content} have no content to read.
+ */
+ public void addContent(ByteBuffer content) {
+ Objects.requireNonNull(content, "'content' cannot be null.");
+
+ int size = content.remaining();
+
+ if (size <= 0) {
+ throw new IllegalArgumentException("There was no content to add to current HTTP response.");
+ }
+
+ final byte[] responseBytes = new byte[content.remaining()];
+ content.get(responseBytes);
+
+ this.contents.put(responseBytes);
+ }
+
+}
diff --git a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/StringUtils.java b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/StringUtils.java
index 9af33ac..95a61c1 100644
--- a/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/StringUtils.java
+++ b/src/main/java/com/microsoft/azure/proton/transport/proxy/impl/StringUtils.java
@@ -7,6 +7,8 @@ package com.microsoft.azure.proton.transport.proxy.impl;
* Utility classes for strings.
*/
class StringUtils {
+ static final String NEW_LINE = "\r\n";
+
static boolean isNullOrEmpty(String string) {
return string == null || string.isEmpty();
}
diff --git a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImplTest.java b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImplTest.java
index 9c0ff0d..35f14a3 100644
--- a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImplTest.java
+++ b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImplTest.java
@@ -180,10 +180,10 @@ public class DigestProxyChallengeProcessorImplTest {
}
private static String generateProxyChallenge(String realm, String nonce, String qop) {
- final String digest = String.format("%s %s realm=\"%s\", nonce=\"%s\", qop=\"%s\", stale=false",
- Constants.PROXY_AUTHENTICATE_HEADER, Constants.DIGEST, realm, nonce, qop);
- final String basic = String.format("%s %s realm=\"%s\"",
- Constants.PROXY_AUTHENTICATE_HEADER, Constants.BASIC, realm);
+ final String digest = String.format("%s realm=\"%s\", nonce=\"%s\", qop=\"%s\", stale=false",
+ Constants.DIGEST, realm, nonce, qop);
+ final String basic = String.format("%s realm=\"%s\"",
+ Constants.BASIC, realm);
return String.join(NEW_LINE,
"HTTP/1.1 407 Proxy Authentication Required",
diff --git a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/HttpStatusLineTest.java b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/HttpStatusLineTest.java
new file mode 100644
index 0000000..3ccff74
--- /dev/null
+++ b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/HttpStatusLineTest.java
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy.impl;
+
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class HttpStatusLineTest {
+
+ /**
+ * Verifies that it successfully parses a valid HTTP status line.
+ */
+ @Test
+ public void validStatusLine() {
+ // Arrange
+ final String line = "HTTP/1.1 200 Connection Established";
+
+ // Act
+ final HttpStatusLine actual = HttpStatusLine.create(line);
+
+ // Assert
+ Assert.assertNotNull(actual);
+
+ Assert.assertEquals(200, actual.getStatusCode());
+ Assert.assertEquals("1.1", actual.getProtocolVersion());
+ Assert.assertEquals("Connection Established", actual.getReason());
+ }
+
+
+ /**
+ * Verifies that status line length is invalid
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"HTTP/1.1 InvalidLength", "HTTP/1.1 Invalid Code", "HTTP/invalid protocol"})
+ public void invalidStatusLine(String line) {
+
+ // Act & Assert
+ Assert.assertThrows(IllegalArgumentException.class, () -> HttpStatusLine.create(line));
+ }
+
+
+}
diff --git a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImplTest.java b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImplTest.java
index 29552ae..6e3431c 100644
--- a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImplTest.java
+++ b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImplTest.java
@@ -3,16 +3,19 @@
package com.microsoft.azure.proton.transport.proxy.impl;
-import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
import org.junit.Assert;
import org.junit.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.util.HashMap;
+import static com.microsoft.azure.proton.transport.proxy.impl.StringUtils.NEW_LINE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
public class ProxyHandlerImplTest {
@Test
public void testCreateProxyRequest() {
@@ -34,79 +37,63 @@ public class ProxyHandlerImplTest {
Assert.assertEquals(expectedProxyRequest, actualProxyRequest);
}
- @ParameterizedTest
- @ValueSource(ints = {200, 201, 202, 203, 204, 205, 206})
- public void testValidateProxyResponseOnSuccess(int httpCode) {
- final String validResponse = "HTTP/1.1 " + httpCode + "Connection Established\r\n"
- + "FiddlerGateway: Direct\r\n"
- + "StartTime: 13:08:21.574\r\n"
- + "Connection: close";
- final ByteBuffer buffer = ByteBuffer.allocate(1024);
- buffer.put(validResponse.getBytes(StandardCharsets.UTF_8));
- buffer.flip();
-
+ @Test
+ public void testValidateProxyResponseOnSuccess() {
+ // Arrange
+ final HttpStatusLine statusLine = HttpStatusLine.create("HTTP/1.1 200 Connection Established");
+ final ProxyResponse response = mock(ProxyResponse.class);
+ when(response.isMissingContent()).thenReturn(false);
+ when(response.getStatus()).thenReturn(statusLine);
final ProxyHandlerImpl proxyHandler = new ProxyHandlerImpl();
- ProxyHandler.ProxyResponseResult responseResult = proxyHandler.validateProxyResponse(buffer);
- Assert.assertTrue(responseResult.getIsSuccess());
- Assert.assertNull(responseResult.getError());
+ // Act
+ final boolean result = proxyHandler.validateProxyResponse(response);
+
+ // Assert
+ Assert.assertTrue(result);
- Assert.assertEquals(0, buffer.remaining());
}
@Test
public void testValidateProxyResponseOnFailure() {
- final String failResponse = String.join("\r\n", "HTTP/1.1 407 Proxy Auth Required",
- "Connection: close",
- "Proxy-Authenticate: Basic realm=\\\"FiddlerProxy (user: 1, pass: 1)\\",
- "Content-Type: text/html",
- "[Fiddler] Proxy Authentication Required.
\r\n");
- final ByteBuffer buffer = ByteBuffer.allocate(1024);
- buffer.put(failResponse.getBytes(StandardCharsets.UTF_8));
- buffer.flip();
+ // Arrange
+ final HttpStatusLine statusLine = HttpStatusLine.create("HTTP/1.1 407 Proxy Auth Required");
+ final String contents = "[Fiddler] Proxy Authentication Required.
";
+ final ByteBuffer encoded = UTF_8.encode(contents);
+ final ProxyResponse response = mock(ProxyResponse.class);
+ when(response.isMissingContent()).thenReturn(false);
+ when(response.getStatus()).thenReturn(statusLine);
+ when(response.getContents()).thenReturn(encoded);
+ when(response.getError()).thenReturn(contents);
final ProxyHandlerImpl proxyHandler = new ProxyHandlerImpl();
- ProxyHandler.ProxyResponseResult responseResult = proxyHandler.validateProxyResponse(buffer);
- Assert.assertTrue(!responseResult.getIsSuccess());
- Assert.assertEquals(failResponse, responseResult.getError());
+ // Act
+ final boolean result = proxyHandler.validateProxyResponse(response);
- Assert.assertEquals(0, buffer.remaining());
- }
-
- @Test
- public void testValidateProxyResponseOnInvalidResponse() {
- final String invalidResponse = String.join("\r\n", "HTTP/1.1 abc Connection Established",
- "HTTP/1.1 200 Connection Established",
- "FiddlerGateway: Direct",
- "StartTime: 13:08:21.574",
- "Connection: close\r\n");
- final ByteBuffer buffer = ByteBuffer.allocate(1024);
- buffer.put(invalidResponse.getBytes(StandardCharsets.UTF_8));
- buffer.flip();
-
- final ProxyHandlerImpl proxyHandler = new ProxyHandlerImpl();
- ProxyHandler.ProxyResponseResult responseResult = proxyHandler.validateProxyResponse(buffer);
-
- Assert.assertTrue(!responseResult.getIsSuccess());
- Assert.assertEquals(invalidResponse, responseResult.getError());
-
- Assert.assertEquals(0, buffer.remaining());
+ // Assert
+ Assert.assertFalse(result);
}
@Test
public void testValidateProxyResponseOnEmptyResponse() {
- final String emptyResponse = "\r\n\r\n";
+ final String emptyResponse = NEW_LINE + NEW_LINE;
final ByteBuffer buffer = ByteBuffer.allocate(1024);
- buffer.put(emptyResponse.getBytes(StandardCharsets.UTF_8));
+ buffer.put(emptyResponse.getBytes(UTF_8));
buffer.flip();
+ final ProxyResponse response = mock(ProxyResponse.class);
+ when(response.isMissingContent()).thenReturn(false);
+ when(response.getStatus()).thenReturn(null);
+ when(response.getContents()).thenReturn(buffer);
+ when(response.getError()).thenReturn(emptyResponse);
+
final ProxyHandlerImpl proxyHandler = new ProxyHandlerImpl();
- ProxyHandler.ProxyResponseResult responseResult = proxyHandler.validateProxyResponse(buffer);
- Assert.assertTrue(!responseResult.getIsSuccess());
- Assert.assertEquals(emptyResponse, responseResult.getError());
+ // Act
+ final boolean result = proxyHandler.validateProxyResponse(response);
- Assert.assertEquals(0, buffer.remaining());
+ // Assert
+ Assert.assertFalse(result);
}
}
diff --git a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImplTest.java b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImplTest.java
index 0efbc8e..86801c5 100644
--- a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImplTest.java
+++ b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImplTest.java
@@ -18,6 +18,9 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,10 +39,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import static com.microsoft.azure.proton.transport.proxy.impl.Constants.BASIC;
import static com.microsoft.azure.proton.transport.proxy.impl.Constants.DIGEST;
-import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHENTICATE_HEADER;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHENTICATE;
import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHORIZATION;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
@@ -59,14 +63,20 @@ import static org.mockito.Mockito.when;
public class ProxyImplTest {
private static final InetSocketAddress PROXY_ADDRESS = InetSocketAddress.createUnresolved("my.host.name", 8888);
private static final java.net.Proxy PROXY = new java.net.Proxy(java.net.Proxy.Type.HTTP, PROXY_ADDRESS);
- private static final int BUFFER_SIZE = 8 * 1024;
+ private static final int BUFFER_SIZE = Constants.PROXY_HANDSHAKE_BUFFER_SIZE;
private static final String USERNAME = "test-user";
private static final String PASSWORD = "test-password!";
+ private static final String BASIC_HEADER = BASIC;
+ private static final String DIGEST_HEADER = String.format("%s realm=\"%s\", nonce=\"A randomly set nonce.\", qop=\"auth\", stale=false",
+ DIGEST, PROXY);
private final Logger logger = LoggerFactory.getLogger(ProxyImplTest.class);
private final Map headers = new HashMap<>();
private ProxySelector originalProxy;
+ @Captor
+ private ArgumentCaptor