Support authentication response are multiple frames (#53)
* Add HttpStatusLine class. * Add new line to StringUtils. * Add Proxy-Authenticate header and value in Constants. * Throws an IllegalArgumentException when unable to parse Status Code. * Update ProxyHandler to validate with a ProxyResponse rather than just a buffer. * Add ProxyResponse and implementation that parses socket information into an HTTP response. * Add tests for ProxyResponseImpl. * Moving shared test utils into a class. * Format MIT header after rebase code * Update ProxyImpl to support authentication from multiple HTTP frames * Remove unused code and fix test * Fix build failure on java 8 * Remove ProxyResponseResult and frameReadState in ProxyResponseImpl * Add UT, reformat logs and change buffer size to 4k * Change HTTPStatusLineTest to HttpStatusLineTest Co-authored-by: Connie <conniey@microsoft.com>
This commit is contained in:
Родитель
4a2bb7c461
Коммит
73fb95f61e
|
@ -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 <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html">RFC 2616</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<String, String> 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);
|
||||
}
|
||||
|
|
|
@ -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<String, List<String>> 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);
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) (?<statusCode>2[0-9]{2})", Pattern.CASE_INSENSITIVE);
|
||||
private final Predicate<String> successStatusLinePredicate = successStatusLine.asPredicate();
|
||||
|
||||
private static final String CONNECTION_ESTABLISHED = "connection established";
|
||||
private static final Set<String> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String> 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> 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<ProxyAuthenticationType> supportedTypes = getAuthenticationTypes(challenge);
|
||||
final Map<String, List<String>> headers = connectResponse.getHeaders();
|
||||
final Set<ProxyAuthenticationType> 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<String> 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<String> challenges,
|
||||
Set<ProxyAuthenticationType> 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<String> 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<String> 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<ProxyAuthenticationType> getAuthenticationTypes(String error) {
|
||||
int index = error.indexOf(Constants.PROXY_AUTHENTICATE_HEADER);
|
||||
|
||||
if (index == -1) {
|
||||
private Set<ProxyAuthenticationType> getAuthenticationTypes(Map<String, List<String>> headers) {
|
||||
if (!headers.containsKey(PROXY_AUTHENTICATE)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<ProxyAuthenticationType> supportedTypes = new HashSet<>();
|
||||
final Set<ProxyAuthenticationType> supportedTypes = new HashSet<>();
|
||||
final List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html">RFC2616</a>
|
||||
*/
|
||||
public final class ProxyResponseImpl implements ProxyResponse {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ProxyResponseImpl.class);
|
||||
|
||||
private final HttpStatusLine status;
|
||||
private final Map<String, List<String>> headers;
|
||||
private final ByteBuffer contents;
|
||||
|
||||
private ProxyResponseImpl(HttpStatusLine status, Map<String, List<String>> 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<String, List<String>> 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<String> 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<String, String> header = parseHeader(line);
|
||||
final List<String> 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<String, String> 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<String, List<String>> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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",
|
||||
"<html><body>[Fiddler] Proxy Authentication Required.<BR></body></html>\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 = "<html><body>[Fiddler] Proxy Authentication Required.<BR></body></html>";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String> headers = new HashMap<>();
|
||||
private ProxySelector originalProxy;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Map<String, String>> additionalHeaders;
|
||||
|
||||
private void initHeaders() {
|
||||
headers.put("header1", "value1");
|
||||
headers.put("header2", "value2");
|
||||
|
@ -75,6 +85,8 @@ public class ProxyImplTest {
|
|||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
originalProxy = ProxySelector.getDefault();
|
||||
|
||||
ProxySelector.setDefault(new ProxySelector() {
|
||||
|
@ -107,6 +119,7 @@ public class ProxyImplTest {
|
|||
|
||||
@After
|
||||
public void teardown() {
|
||||
Mockito.framework().clearInlineMocks();
|
||||
ProxySelector.setDefault(originalProxy);
|
||||
}
|
||||
|
||||
|
@ -326,13 +339,14 @@ public class ProxyImplTest {
|
|||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, mockHandler, mock(TransportImpl.class));
|
||||
TransportInput mockInput = mock(TransportInput.class);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(mockInput, mock(TransportOutput.class));
|
||||
ProxyHandler.ProxyResponseResult mockResponse = mock(ProxyHandler.ProxyResponseResult.class);
|
||||
|
||||
when(mockHandler.createProxyRequest(any(), any())).thenReturn("proxy request");
|
||||
|
||||
when(mockResponse.getIsSuccess()).thenReturn(true);
|
||||
when(mockResponse.getError()).thenReturn(null);
|
||||
when(mockHandler.validateProxyResponse(any())).thenReturn(mockResponse);
|
||||
when(mockHandler.validateProxyResponse(any())).thenReturn(true);
|
||||
|
||||
final String[] statusLine = new String[]{"HTTP/1.1", "200", "Connection Established"};
|
||||
String response = getProxyResponse(statusLine, new ArrayList<>());
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
transportWrapper.pending();
|
||||
|
@ -351,13 +365,14 @@ public class ProxyImplTest {
|
|||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, mockHandler, mockTransport);
|
||||
TransportInput mockInput = mock(TransportInput.class);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(mockInput, mock(TransportOutput.class));
|
||||
ProxyHandler.ProxyResponseResult mockResponse = mock(ProxyHandler.ProxyResponseResult.class);
|
||||
|
||||
when(mockHandler.createProxyRequest(any(), any())).thenReturn("proxy request");
|
||||
|
||||
when(mockResponse.getIsSuccess()).thenReturn(false);
|
||||
when(mockResponse.getError()).thenReturn("proxy failure response");
|
||||
when(mockHandler.validateProxyResponse(any())).thenReturn(mockResponse);
|
||||
final String[] statusLine = new String[]{"HTTP/1.1", "500", "Internal Server Error"};
|
||||
final List<String> authentications = new ArrayList<>();
|
||||
String response = getProxyResponse(statusLine, authentications);
|
||||
setInputBuffer(proxyImpl, response);
|
||||
when(mockHandler.validateProxyResponse(any())).thenReturn(false);
|
||||
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
transportWrapper.pending();
|
||||
|
@ -679,13 +694,15 @@ public class ProxyImplTest {
|
|||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, handler, underlyingTransport);
|
||||
TransportInput input = mock(TransportInput.class);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(input, mock(TransportOutput.class));
|
||||
ProxyHandler.ProxyResponseResult mockResponse = mock(ProxyHandler.ProxyResponseResult.class);
|
||||
|
||||
when(handler.createProxyRequest(any(), any())).thenReturn("proxy request");
|
||||
when(handler.validateProxyResponse(any())).thenReturn(mockResponse);
|
||||
when(handler.validateProxyResponse(any())).thenReturn(false);
|
||||
|
||||
when(mockResponse.getIsSuccess()).thenReturn(false);
|
||||
when(mockResponse.getError()).thenReturn(getProxyChallenge(true, false));
|
||||
final String[] statusLine = new String[]{"HTTP/1.1", "407", "Proxy Authentication Required"};
|
||||
final List<String> authentications = new ArrayList<>();
|
||||
authentications.add(BASIC_HEADER);
|
||||
final String response = getProxyResponse(statusLine, authentications);
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
// Act and Assert
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
|
@ -699,9 +716,7 @@ public class ProxyImplTest {
|
|||
}
|
||||
|
||||
/**
|
||||
* Verifies that if we explicitly set ProxyAuthenticationType.NONE and the proxy asks for verification then we fail.
|
||||
* This also covers the case where the proxy configuration suggests one auth method, but it is not supported in the
|
||||
* proxy challenge.
|
||||
* Verifies that if we configure proxy authentication type but the proxy does not ask for verification then we fail.
|
||||
*/
|
||||
@Test
|
||||
public void authenticationNoAuthMismatchClosesTail() {
|
||||
|
@ -713,13 +728,14 @@ public class ProxyImplTest {
|
|||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, handler, underlyingTransport);
|
||||
TransportInput input = mock(TransportInput.class);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(input, mock(TransportOutput.class));
|
||||
ProxyHandler.ProxyResponseResult mockResponse = mock(ProxyHandler.ProxyResponseResult.class);
|
||||
|
||||
when(handler.createProxyRequest(any(), any())).thenReturn("proxy request");
|
||||
when(handler.validateProxyResponse(any())).thenReturn(mockResponse);
|
||||
when(handler.validateProxyResponse(any())).thenReturn(true);
|
||||
|
||||
final String[] statusLine = new String[]{"HTTP/1.1", "200", "Connection Established"};
|
||||
String response = getProxyResponse(statusLine, new ArrayList<>());
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
when(mockResponse.getIsSuccess()).thenReturn(true);
|
||||
when(mockResponse.getError()).thenReturn(null);
|
||||
|
||||
// Act and Assert
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
|
@ -737,7 +753,6 @@ public class ProxyImplTest {
|
|||
* configured auth method.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void authenticationWithProxyConfiguration() {
|
||||
// Arrange
|
||||
ProxyConfiguration configuration = new ProxyConfiguration(ProxyAuthenticationType.BASIC, PROXY, USERNAME, PASSWORD);
|
||||
|
@ -747,13 +762,16 @@ public class ProxyImplTest {
|
|||
TransportOutput output = mock(TransportOutput.class);
|
||||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, handler, underlyingTransport);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(mock(TransportInput.class), output);
|
||||
ProxyHandler.ProxyResponseResult mockResponse = mock(ProxyHandler.ProxyResponseResult.class);
|
||||
|
||||
when(handler.createProxyRequest(any(), any())).thenReturn("proxy request", "proxy request2");
|
||||
when(handler.validateProxyResponse(any())).thenReturn(mockResponse);
|
||||
when(handler.validateProxyResponse(any())).thenReturn(false, true);
|
||||
|
||||
when(mockResponse.getIsSuccess()).thenReturn(false, true);
|
||||
when(mockResponse.getError()).thenReturn(getProxyChallenge(true, true), "Success.");
|
||||
String[] statusLine = new String[]{"HTTP/1.1", "407", "Proxy Authentication Required"};
|
||||
List<String> authentications = new ArrayList<>();
|
||||
authentications.add(BASIC_HEADER);
|
||||
authentications.add(DIGEST_HEADER);
|
||||
String response = getProxyResponse(statusLine, authentications);
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
// Act and Assert
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
|
@ -769,6 +787,10 @@ public class ProxyImplTest {
|
|||
clearOutputBuffer(proxyImpl);
|
||||
transportWrapper.pending();
|
||||
|
||||
statusLine = new String[]{"HTTP/1.1", "200", "Connection Established"};
|
||||
response = getProxyResponse(statusLine, new ArrayList<>());
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
Assert.assertTrue(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CHALLENGE_RESPONDED, proxyImpl.getProxyState());
|
||||
transportWrapper.process();
|
||||
|
@ -776,46 +798,42 @@ public class ProxyImplTest {
|
|||
Assert.assertFalse(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CONNECTED, proxyImpl.getProxyState());
|
||||
|
||||
ArgumentCaptor<Map> captor = ArgumentCaptor.forClass(Map.class);
|
||||
verify(handler, times(2)).createProxyRequest(
|
||||
argThat(string -> string != null && string.equals(PROXY_ADDRESS.getHostName())), (Map<String, String>) captor.capture());
|
||||
argThat(string -> string != null && string.equals(PROXY_ADDRESS.getHostName())), additionalHeaders.capture());
|
||||
|
||||
boolean foundHeader = false;
|
||||
for (Map map : captor.getAllValues()) {
|
||||
if (!map.containsKey(PROXY_AUTHORIZATION)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String value = (String) map.get(PROXY_AUTHORIZATION);
|
||||
if (value.trim().startsWith(BASIC)) {
|
||||
foundHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Optional<Map<String, String>> matching = additionalHeaders.getAllValues()
|
||||
.stream()
|
||||
.filter(map -> map.containsKey(PROXY_AUTHORIZATION)
|
||||
&& map.get(PROXY_AUTHORIZATION).trim().startsWith(BASIC))
|
||||
.findFirst();
|
||||
|
||||
Assert.assertTrue(foundHeader);
|
||||
Assert.assertTrue(matching.isPresent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when we use the system defaults and both are offered, then we will use the the DIGEST.
|
||||
* Verifies that when we use the system defaults and both are offered, then we will use the DIGEST.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void authenticationWithSystemDefaults() {
|
||||
// Arrange
|
||||
ProxyImpl proxyImpl = new ProxyImpl();
|
||||
ProxyHandler handler = mock(ProxyHandler.class);
|
||||
TransportImpl underlyingTransport = mock(TransportImpl.class);
|
||||
TransportOutput output = mock(TransportOutput.class);
|
||||
TransportInput transportInput = mock(TransportInput.class);
|
||||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, handler, underlyingTransport);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(mock(TransportInput.class), output);
|
||||
ProxyHandler.ProxyResponseResult mockResponse = mock(ProxyHandler.ProxyResponseResult.class);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(transportInput, output);
|
||||
|
||||
when(handler.createProxyRequest(any(), any())).thenReturn("proxy request", "proxy request2");
|
||||
when(handler.validateProxyResponse(any())).thenReturn(mockResponse);
|
||||
when(handler.validateProxyResponse(any())).thenReturn(false, true);
|
||||
|
||||
when(mockResponse.getIsSuccess()).thenReturn(false, true);
|
||||
when(mockResponse.getError()).thenReturn(getProxyChallenge(true, true), "Success.");
|
||||
String[] statusLine = new String[]{"HTTP/1.1", "407", "Proxy Authentication Required"};
|
||||
List<String> authentications = new ArrayList<>();
|
||||
authentications.add(BASIC_HEADER);
|
||||
authentications.add(DIGEST_HEADER);
|
||||
String response = getProxyResponse(statusLine, authentications);
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
// Act and Assert
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
|
@ -831,6 +849,10 @@ public class ProxyImplTest {
|
|||
clearOutputBuffer(proxyImpl);
|
||||
transportWrapper.pending();
|
||||
|
||||
statusLine = new String[]{"HTTP/1.1", "200", "Connection Established"};
|
||||
response = getProxyResponse(statusLine, new ArrayList<>());
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
Assert.assertTrue(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CHALLENGE_RESPONDED, proxyImpl.getProxyState());
|
||||
transportWrapper.process();
|
||||
|
@ -838,24 +860,87 @@ public class ProxyImplTest {
|
|||
Assert.assertFalse(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CONNECTED, proxyImpl.getProxyState());
|
||||
|
||||
ArgumentCaptor<Map> captor = ArgumentCaptor.forClass(Map.class);
|
||||
verify(handler, times(2)).createProxyRequest(
|
||||
argThat(string -> string != null && string.equals(PROXY_ADDRESS.getHostName())), (Map<String, String>) captor.capture());
|
||||
argThat(string -> string != null && string.equals(PROXY_ADDRESS.getHostName())), additionalHeaders.capture());
|
||||
|
||||
boolean foundHeader = false;
|
||||
for (Map map : captor.getAllValues()) {
|
||||
if (!map.containsKey(PROXY_AUTHORIZATION)) {
|
||||
continue;
|
||||
}
|
||||
final Optional<Map<String, String>> matching = additionalHeaders.getAllValues()
|
||||
.stream()
|
||||
.filter(map -> map.containsKey(PROXY_AUTHORIZATION)
|
||||
&& map.get(PROXY_AUTHORIZATION).trim().startsWith(DIGEST))
|
||||
.findFirst();
|
||||
|
||||
String value = (String) map.get(PROXY_AUTHORIZATION);
|
||||
if (value.trim().startsWith(DIGEST)) {
|
||||
foundHeader = true;
|
||||
break;
|
||||
}
|
||||
Assert.assertTrue(matching.isPresent());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verifies that when proxy authentication response are transfer in multiple frames.
|
||||
*/
|
||||
@Test
|
||||
public void authenticationResponseWithMultipleFrames() {
|
||||
// Arrange
|
||||
ProxyImpl proxyImpl = new ProxyImpl();
|
||||
ProxyHandler handler = mock(ProxyHandler.class);
|
||||
TransportImpl underlyingTransport = mock(TransportImpl.class);
|
||||
TransportOutput output = mock(TransportOutput.class);
|
||||
TransportInput transportInput = mock(TransportInput.class);
|
||||
proxyImpl.configure(PROXY_ADDRESS.getHostName(), headers, handler, underlyingTransport);
|
||||
TransportWrapper transportWrapper = proxyImpl.wrap(transportInput, output);
|
||||
|
||||
when(handler.createProxyRequest(any(), any())).thenReturn("proxy request", "proxy request2");
|
||||
when(handler.validateProxyResponse(any())).thenReturn(false, true);
|
||||
|
||||
String[] statusLine = new String[]{"HTTP/1.1", "407", "Proxy Authentication Required"};
|
||||
List<String> authentications = new ArrayList<>();
|
||||
authentications.add(BASIC_HEADER);
|
||||
authentications.add(DIGEST_HEADER);
|
||||
//Create a body which over buffer size so that it could be cut into multiple frames
|
||||
//Here body is (buffer size * 2) bytes, and consider header size,
|
||||
//it will create 3 frames for proxy response
|
||||
String body = new String(new char[BUFFER_SIZE]).replace('\0', 't');
|
||||
List<String> responses = getProxyResponseFrames(statusLine, authentications, body);
|
||||
|
||||
// Act and Assert
|
||||
Assert.assertEquals(3, responses.size());
|
||||
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_NOT_STARTED, proxyImpl.getProxyState());
|
||||
transportWrapper.pending();
|
||||
|
||||
for (String response : responses) {
|
||||
setInputBuffer(proxyImpl, response);
|
||||
Assert.assertTrue(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CONNECTING, proxyImpl.getProxyState());
|
||||
transportWrapper.process();
|
||||
}
|
||||
|
||||
Assert.assertTrue(foundHeader);
|
||||
|
||||
// At this point, we've gotten the correct challenger and set the header we want to respond with. We want to
|
||||
// zero out the output buffer so that it'll write the headers when getting the request from the proxy handler.
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CHALLENGE, proxyImpl.getProxyState());
|
||||
clearOutputBuffer(proxyImpl);
|
||||
transportWrapper.pending();
|
||||
|
||||
statusLine = new String[]{"HTTP/1.1", "200", "Connection Established"};
|
||||
String response = getProxyResponse(statusLine, new ArrayList<>());
|
||||
setInputBuffer(proxyImpl, response);
|
||||
|
||||
Assert.assertTrue(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CHALLENGE_RESPONDED, proxyImpl.getProxyState());
|
||||
transportWrapper.process();
|
||||
|
||||
Assert.assertFalse(proxyImpl.getIsHandshakeInProgress());
|
||||
Assert.assertEquals(Proxy.ProxyState.PN_PROXY_CONNECTED, proxyImpl.getProxyState());
|
||||
|
||||
verify(handler, times(2)).createProxyRequest(
|
||||
argThat(string -> string != null && string.equals(PROXY_ADDRESS.getHostName())), additionalHeaders.capture());
|
||||
|
||||
final Optional<Map<String, String>> matching = additionalHeaders.getAllValues()
|
||||
.stream()
|
||||
.filter(map -> map.containsKey(PROXY_AUTHORIZATION)
|
||||
&& map.get(PROXY_AUTHORIZATION).trim().startsWith(DIGEST))
|
||||
.findFirst();
|
||||
|
||||
Assert.assertTrue(matching.isPresent());
|
||||
}
|
||||
|
||||
private static int getConnectRequestLength(String host, Map<String, String> headers) {
|
||||
|
@ -903,26 +988,53 @@ public class ProxyImplTest {
|
|||
}
|
||||
}
|
||||
|
||||
private static String getProxyChallenge(boolean includeBasic, boolean includeDigest) {
|
||||
final String newLine = "\n";
|
||||
StringBuilder builder = new StringBuilder("HTTP/1.1 407 Proxy Authentication Required");
|
||||
builder.append(newLine);
|
||||
builder.append("Date: Sun, 05 May 2019 07:28:00 GMT");
|
||||
builder.append(newLine);
|
||||
|
||||
if (includeBasic) {
|
||||
builder.append(String.join(" ", PROXY_AUTHENTICATE_HEADER, BASIC));
|
||||
builder.append(newLine);
|
||||
private void setInputBuffer(ProxyImpl proxyImpl, String value) {
|
||||
final String inputBufferName = "inputBuffer";
|
||||
try {
|
||||
Field inputBuffer = ProxyImpl.class.getDeclaredField(inputBufferName);
|
||||
inputBuffer.setAccessible(true);
|
||||
|
||||
ByteBuffer buffer = (ByteBuffer) inputBuffer.get(proxyImpl);
|
||||
buffer.put(value.getBytes());
|
||||
} catch (NoSuchFieldException e) {
|
||||
if (logger.isErrorEnabled()) {
|
||||
logger.error("Could not locate field '{}' on ProxyImpl class. Exception: {}", inputBufferName, e);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
if (logger.isErrorEnabled()) {
|
||||
logger.error("Could not fetch byte buffer from object.", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeDigest) {
|
||||
builder.append(String.format("%s %s realm=\"%s\", nonce=\"A randomly set nonce.\", qop=\"auth\", stale=false",
|
||||
PROXY_AUTHENTICATE_HEADER, DIGEST, PROXY));
|
||||
builder.append(newLine);
|
||||
}
|
||||
|
||||
builder.append(newLine);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String getProxyResponse(String[] statusLine, List<String> authentications) {
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
|
||||
if (!authentications.isEmpty()) {
|
||||
headers.put(PROXY_AUTHENTICATE, authentications);
|
||||
}
|
||||
|
||||
return TestUtils.createProxyResponse(statusLine, headers);
|
||||
}
|
||||
|
||||
private List<String> getProxyResponseFrames(String[] statusLine, List<String> authentications, String body) {
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
|
||||
if (!authentications.isEmpty()) {
|
||||
headers.put(PROXY_AUTHENTICATE, authentications);
|
||||
}
|
||||
|
||||
String response = TestUtils.createProxyResponse(statusLine, headers, body);
|
||||
|
||||
//Split response into frames base on buffer in characters size
|
||||
int characters = BUFFER_SIZE / 2;
|
||||
List<String> frames = new ArrayList<>();
|
||||
for (int i = 0; i < response.length(); i += characters) {
|
||||
frames.add(response.substring(i, Math.min(i + characters, response.length())));
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
// 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 org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.microsoft.azure.proton.transport.proxy.impl.StringUtils.NEW_LINE;
|
||||
|
||||
public class ProxyResponseImplTest {
|
||||
/**
|
||||
* Verifies that it successfully parses a valid HTTP response.
|
||||
*/
|
||||
@Test
|
||||
public void validResponse() {
|
||||
// Arrange
|
||||
final String[] statusLine = new String[]{"HTTP/1.1", "200", "Connection Established"};
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("FiddlerGateway", Collections.singletonList("Direct"));
|
||||
headers.put("StartTime", Collections.singletonList("13:08:21.574"));
|
||||
headers.put("Connection", Collections.singletonList("close"));
|
||||
|
||||
final String response = TestUtils.createProxyResponse(statusLine, headers);
|
||||
final ByteBuffer contents = TestUtils.ENCODING.encode(response);
|
||||
|
||||
// Act
|
||||
final ProxyResponse actual = ProxyResponseImpl.create(contents);
|
||||
|
||||
// Assert
|
||||
Assert.assertNotNull(actual);
|
||||
|
||||
final HttpStatusLine status = actual.getStatus();
|
||||
Assert.assertEquals("1.1", status.getProtocolVersion());
|
||||
Assert.assertEquals(statusLine[2], status.getReason());
|
||||
Assert.assertEquals(Integer.parseInt(statusLine[1]), status.getStatusCode());
|
||||
|
||||
Assert.assertFalse(actual.isMissingContent());
|
||||
|
||||
final Map<String, List<String>> actualHeaders = actual.getHeaders();
|
||||
Assert.assertEquals(headers.size(), actualHeaders.size());
|
||||
|
||||
headers.forEach((key, value) -> {
|
||||
final List<String> actualValue = actualHeaders.get(key);
|
||||
|
||||
Assert.assertTrue(actualHeaders.containsKey(key));
|
||||
Assert.assertNotNull(actualValue);
|
||||
Assert.assertEquals(1, actualValue.size());
|
||||
Assert.assertEquals(value.get(0), actualValue.get(0));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an exception is thrown when buffer is empty
|
||||
*/
|
||||
@Test
|
||||
public void invalidBuffer() {
|
||||
// Arrange
|
||||
final ByteBuffer contents = ByteBuffer.allocate(0);
|
||||
|
||||
// Act & Assert
|
||||
Assert.assertThrows(IllegalArgumentException.class, () -> ProxyResponseImpl.create(contents));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an exception is thrown when the header is invalid.
|
||||
*/
|
||||
@Test
|
||||
public void invalidHeader() {
|
||||
// Arrange
|
||||
final String[] statusLine = new String[]{"HTTP/1.1", "abc", "Connection Established"};
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
headers.put("FiddlerGateway", Collections.singletonList("Direct"));
|
||||
headers.put("StartTime", Collections.singletonList("13:08:21.574"));
|
||||
headers.put("Connection", Collections.singletonList("close"));
|
||||
|
||||
final String response = TestUtils.createProxyResponse(statusLine, headers);
|
||||
final ByteBuffer contents = TestUtils.ENCODING.encode(response);
|
||||
|
||||
// Act & Assert
|
||||
IllegalArgumentException thrown = Assert.assertThrows(IllegalArgumentException.class,
|
||||
() -> ProxyResponseImpl.create(contents));
|
||||
Assert.assertEquals(NumberFormatException.class, thrown.getCause().getClass());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that we can parse an empty response.
|
||||
*/
|
||||
@Test
|
||||
public void emptyResponse() {
|
||||
// Arrange
|
||||
final String emptyResponse = NEW_LINE + NEW_LINE;
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
buffer.put(emptyResponse.getBytes(TestUtils.ENCODING));
|
||||
buffer.flip();
|
||||
|
||||
// Act
|
||||
final ProxyResponse response = ProxyResponseImpl.create(buffer);
|
||||
|
||||
// Assert
|
||||
Assert.assertNotNull(response);
|
||||
Assert.assertNull(response.getStatus());
|
||||
Assert.assertFalse(response.isMissingContent());
|
||||
Assert.assertNotNull(response.getHeaders());
|
||||
Assert.assertEquals(0, response.getHeaders().size());
|
||||
Assert.assertEquals(0, response.getContents().position());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
package com.microsoft.azure.proton.transport.proxy.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.microsoft.azure.proton.transport.proxy.impl.StringUtils.NEW_LINE;
|
||||
|
||||
final class TestUtils {
|
||||
/**
|
||||
* Encoding for the HTTP proxy response.
|
||||
*/
|
||||
static final Charset ENCODING = StandardCharsets.UTF_8;
|
||||
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
private static final String CONTENT_TYPE_TEXT = "text/plain";
|
||||
private static final String CONTENT_LENGTH = "Content-Length";
|
||||
private static final String HEADER_FORMAT = "%s: %s" + NEW_LINE;
|
||||
|
||||
private TestUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a proxy HTTP response and returns it as a string.
|
||||
*
|
||||
* @param statusLine HTTP status line to create the proxy response with.
|
||||
* @param headers A set of headers to add to the proxy response.
|
||||
* @return A string representing the contents of the HTTP response.
|
||||
*/
|
||||
static String createProxyResponse(String[] statusLine, Map<String, List<String>> headers) {
|
||||
return createProxyResponse(statusLine, headers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a proxy HTTP response and returns it as a string. If there is content, {@link #CONTENT_LENGTH} and
|
||||
* {@link #CONTENT_TYPE} headers are added to the {@code headers} parameter.
|
||||
*
|
||||
* @param statusLine HTTP status line to create the proxy response with.
|
||||
* @param headers A set of headers to add to the proxy response.
|
||||
* @param body Optional HTTP content body.
|
||||
* @return A string representing the contents of the HTTP response.
|
||||
*/
|
||||
static String createProxyResponse(String[] statusLine, Map<String, List<String>> headers, String body) {
|
||||
final ByteBuffer encoded;
|
||||
if (body != null) {
|
||||
//Add empty line to end body
|
||||
body += NEW_LINE;
|
||||
|
||||
encoded = ENCODING.encode(body);
|
||||
final int size = encoded.remaining();
|
||||
|
||||
headers.put(CONTENT_TYPE, Collections.singletonList(CONTENT_TYPE_TEXT));
|
||||
headers.put(CONTENT_LENGTH, Collections.singletonList(Integer.toString(size)));
|
||||
}
|
||||
|
||||
final StringBuilder formattedHeaders = headers.entrySet()
|
||||
.stream()
|
||||
.collect(StringBuilder::new,
|
||||
(builder, entry) -> entry.getValue()
|
||||
.forEach(value -> builder.append(String.format(HEADER_FORMAT, entry.getKey(), value))),
|
||||
StringBuilder::append);
|
||||
|
||||
String response = String.join(NEW_LINE,
|
||||
String.join(" ", statusLine),
|
||||
formattedHeaders.toString(),
|
||||
NEW_LINE); // The empty new line that ends the HTTP headers.
|
||||
|
||||
if (body != null) {
|
||||
response += body;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
Загрузка…
Ссылка в новой задаче