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:
Kun Liu 2022-03-15 03:41:28 +08:00 коммит произвёл GitHub
Родитель 4a2bb7c461
Коммит 73fb95f61e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 997 добавлений и 263 удалений

Просмотреть файл

@ -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;
}
}