[Git][java-team/qpid-proton-j-extensions][master] 6 commits: New upstream version 1.2.5
Tony Mancill (@tmancill)
gitlab at salsa.debian.org
Mon Nov 25 05:14:32 GMT 2024
Tony Mancill pushed to branch master at Debian Java Maintainers / qpid-proton-j-extensions
Commits:
ea27fddc by tony mancill at 2024-11-24T20:49:27-08:00
New upstream version 1.2.5
- - - - -
35fc4a2b by tony mancill at 2024-11-24T20:49:28-08:00
Update upstream source from tag 'upstream/1.2.5'
Update to upstream version '1.2.5'
with Debian dir 7f651997cc967e08486cfd29f9fa0dfe62f0bdcd
- - - - -
0f90ab40 by tony mancill at 2024-11-24T21:03:11-08:00
Add patch for deprecated Mockito method (Closes: #1086310)
- - - - -
df0cb178 by tony mancill at 2024-11-24T21:03:11-08:00
Freshen years in debian/copyright
- - - - -
0e833d3c by tony mancill at 2024-11-24T21:03:11-08:00
Bump Standards-Version to 4.7.0
- - - - -
842738d2 by tony mancill at 2024-11-24T21:08:33-08:00
Prepare changelog for upload
- - - - -
26 changed files:
- + SECURITY.md
- ci.yml
- debian/changelog
- debian/control
- debian/copyright
- + debian/patches/02-mockito-compat.patch
- debian/patches/series
- + eng/templates/1es-redirect.yml
- + eng/templates/image.yml
- + eng/templates/test-steps.yml
- pom.xml
- + src/main/java/com/microsoft/azure/proton/transport/proxy/HttpStatusLine.java
- src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyHandler.java
- + src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyResponse.java
- src/main/java/com/microsoft/azure/proton/transport/proxy/impl/Constants.java
- src/main/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImpl.java
- src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImpl.java
- src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java
- + src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImpl.java
- src/main/java/com/microsoft/azure/proton/transport/proxy/impl/StringUtils.java
- src/test/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImplTest.java
- + src/test/java/com/microsoft/azure/proton/transport/proxy/impl/HttpStatusLineTest.java
- src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImplTest.java
- src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImplTest.java
- + src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImplTest.java
- + src/test/java/com/microsoft/azure/proton/transport/proxy/impl/TestUtils.java
Changes:
=====================================
SECURITY.md
=====================================
@@ -0,0 +1,41 @@
+<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK -->
+
+## Security
+
+Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
+
+If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
+
+## Reporting Security Issues
+
+**Please do not report security vulnerabilities through public GitHub issues.**
+
+Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
+
+If you prefer to submit without logging in, send email to [secure at microsoft.com](mailto:secure at microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
+
+You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
+
+Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
+
+ * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
+ * Full paths of source file(s) related to the manifestation of the issue
+ * The location of the affected source code (tag/branch/commit or direct URL)
+ * Any special configuration required to reproduce the issue
+ * Step-by-step instructions to reproduce the issue
+ * Proof-of-concept or exploit code (if possible)
+ * Impact of the issue, including how an attacker might exploit the issue
+
+This information will help us triage your report more quickly.
+
+If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
+
+## Preferred Languages
+
+We prefer all communications to be in English.
+
+## Policy
+
+Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
+
+<!-- END MICROSOFT SECURITY.MD BLOCK -->
=====================================
ci.yml
=====================================
@@ -1,9 +1,3 @@
-resources:
- repositories:
- - repository: azure-sdk-build-tools
- type: git
- name: internal/azure-sdk-build-tools
-
trigger:
branches:
include:
@@ -20,116 +14,140 @@ pr:
- release/*
- hotfix/*
-variables:
- DefaultOptions: '--batch-mode --fail-at-end -Dmaven.wagon.http.pool=false'
- LoggingOptions: '-Dorg.slf4j.simpleLogger.defaultLogLevel=error -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn'
- MemoryOptions: '-Xmx3072m'
-
-stages:
- - stage: Build
- jobs:
- - job: 'Build'
- pool:
- vmImage: 'ubuntu-18.04'
- strategy:
- matrix:
- Java 8:
- ArtifactName: 'java8-packages'
- ProfileFlag: '-Djava8'
- JavaVersion: '1.8'
- # We name this 'packages' because it is the default version we want to ship with.
- Java LTS:
- ArtifactName: 'packages'
- ProfileFlag: '-Djava-lts'
- JavaVersion: '1.11'
-
- steps:
- - task: Maven at 3
- displayName: 'Build and Package'
- inputs:
- mavenPomFile: 'pom.xml'
- goals: 'deploy'
- options: '$(DefaultOptions) $(ProfileFlag) -T 1C -DskipTests -Dgpg.skip -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true -Dspotbugs.skip=true --settings eng/settings.xml -DaltDeploymentRepository="local::default::file:///${project.basedir}/output"'
- mavenOptions: '$(LoggingOptions) $(MemoryOptions)'
- javaHomeOption: 'JDKVersion'
- jdkVersionOption: $(JavaVersion)
- jdkArchitectureOption: 'x64'
- publishJUnitResults: false
-
- - task: Maven at 3
- displayName: 'Run SpotBugs, Checkstyle, and Javadoc'
- inputs:
- mavenPomFile: pom.xml
- options: '$(DefaultOptions) --no-transfer-progress -DskipTests -Dgpg.skip'
- mavenOptions: '$(MemoryOptions)'
- javaHomeOption: 'JDKVersion'
- jdkVersionOption: $(JavaVersion)
- jdkArchitectureOption: 'x64'
- publishJUnitResults: false
- goals: 'verify'
-
- - script: |
- cp output/com/microsoft/azure/qpid-proton-j-extensions/**/* $(Build.ArtifactStagingDirectory)
- rm $(Build.ArtifactStagingDirectory)/*.sha1
- rm $(Build.ArtifactStagingDirectory)/*.md5
- displayName: Flatten and copy build outputs
-
- - publish: $(Build.ArtifactStagingDirectory)
- artifact: $(ArtifactName)
- displayName: 'Publish outputs to $(ArtifactName) artifact'
-
- - job: 'Test'
- strategy:
- matrix:
- Linux - Java 8:
- OSName: 'Linux'
- OSVmImage: 'ubuntu-18.04'
- ProfileFlag: '-Djava8'
- JavaVersion: '1.8'
- macOS - Java 8:
- OSName: 'macOS'
- OSVmImage: 'macOS-10.15'
- ProfileFlag: '-Djava8'
- JavaVersion: '1.8'
- Windows - Java 8:
- OSName: 'Windows'
- OSVmImage: 'vs2017-win2016'
- ProfileFlag: '-Djava8'
- JavaVersion: '1.8'
- Linux - Java LTS:
- OSName: 'Linux'
- OSVmImage: 'ubuntu-18.04'
- ProfileFlag: '-Djava-lts'
- JavaVersion: '1.11'
- macOS - Java LTS:
- OSName: 'macOS'
- OSVmImage: 'macOS-10.15'
- ProfileFlag: '-Djava-lts'
- JavaVersion: '1.11'
- Windows - Java LTS:
- OSName: 'Windows'
- OSVmImage: 'vs2017-win2016'
- ProfileFlag: '-Djava-lts'
- JavaVersion: '1.11'
-
- pool:
- vmImage: $(OSVmImage)
-
- steps:
- - task: Maven at 3
- displayName: 'Run Tests'
- inputs:
- mavenPomFile: 'pom.xml'
- options: '$(DefaultOptions) $(ProfileFlag) --settings eng/settings.xml'
- mavenOptions: '-Xmx3072m $(LoggingOptions)'
- javaHomeOption: 'JDKVersion'
- jdkVersionOption: $(JavaVersion)
- jdkArchitectureOption: 'x64'
- publishJUnitResults: false
- goals: 'test'
-
- - task: PublishTestResults at 2
- condition: succeededOrFailed()
- inputs:
- mergeTestResults: true
- testRunTitle: '$(OSName) on Java $(JavaVersion)'
+extends:
+ template: /eng/templates/1es-redirect.yml
+ parameters:
+ stages:
+ - stage: Build
+ variables:
+ - name: DefaultOptions
+ value: '--batch-mode --fail-at-end -Dmaven.wagon.http.pool=false'
+ - name: LoggingOptions
+ value: '-Dorg.slf4j.simpleLogger.defaultLogLevel=error -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn'
+ - name: MemoryOptions
+ value: '-Xmx3072m'
+ - template: /eng/templates/image.yml
+ jobs:
+ - job: 'Build'
+ pool:
+ name: $(LINUXPOOL)
+ image: $(LINUXVMIMAGE)
+ os: linux
+
+ strategy:
+ matrix:
+ Java 8:
+ ArtifactName: 'java8-packages'
+ ProfileFlag: '-Djava8'
+ JavaVersion: '1.8'
+ # We name this 'packages' because it is the default version we want to ship with.
+ Java LTS:
+ ArtifactName: 'packages'
+ ProfileFlag: '-Djava-lts'
+ JavaVersion: '1.11'
+
+ steps:
+ - task: Maven at 3
+ displayName: 'Build and Package'
+ inputs:
+ mavenPomFile: 'pom.xml'
+ goals: 'deploy'
+ options: '$(DefaultOptions) $(ProfileFlag) -T 1C -DskipTests -Dgpg.skip -Dcheckstyle.skip=true -Dspotbugs.skip=true --settings eng/settings.xml -DaltDeploymentRepository="local::default::file:///${project.basedir}/output"'
+ mavenOptions: '$(LoggingOptions) $(MemoryOptions)'
+ javaHomeOption: 'JDKVersion'
+ jdkVersionOption: $(JavaVersion)
+ jdkArchitectureOption: 'x64'
+ publishJUnitResults: false
+
+ - task: Maven at 3
+ displayName: 'Run SpotBugs, Checkstyle, and Javadoc'
+ inputs:
+ mavenPomFile: pom.xml
+ options: '$(DefaultOptions) --no-transfer-progress -DskipTests -Dgpg.skip'
+ mavenOptions: '$(MemoryOptions)'
+ javaHomeOption: 'JDKVersion'
+ jdkVersionOption: $(JavaVersion)
+ jdkArchitectureOption: 'x64'
+ publishJUnitResults: false
+ goals: 'verify'
+
+ - script: |
+ cp output/com/microsoft/azure/qpid-proton-j-extensions/**/* $(Build.ArtifactStagingDirectory)
+ rm $(Build.ArtifactStagingDirectory)/*.sha1
+ rm $(Build.ArtifactStagingDirectory)/*.md5
+ displayName: Flatten and copy build outputs
+
+ - task: 1ES.PublishPipelineArtifact at 1
+ displayName: 'Publish outputs to $(ArtifactName) artifact'
+ inputs:
+ artifactName: $(ArtifactName)
+ targetPath: $(Build.ArtifactStagingDirectory)
+
+ - job:
+ displayName: 'Test'
+ strategy:
+ matrix:
+ Linux - Java 8:
+ OSName: 'Linux'
+ OSVmImage: 'ubuntu-22.04'
+ ProfileFlag: '-Djava8'
+ JavaVersion: '1.8'
+ Linux - Java LTS:
+ OSName: 'Linux'
+ OSVmImage: 'ubuntu-22.04'
+ ProfileFlag: '-Djava-lts'
+ JavaVersion: '1.11'
+
+ pool:
+ name: $(LINUXPOOL)
+ image: $(LINUXVMIMAGE)
+ os: linux
+
+ steps:
+ - template: /eng/templates/test-steps.yml
+
+ - job:
+ displayName: 'Test'
+ strategy:
+ matrix:
+ Windows - Java 8:
+ OSName: 'Windows'
+ OSVmImage: 'windows-2022'
+ ProfileFlag: '-Djava8'
+ JavaVersion: '1.8'
+ Windows - Java LTS:
+ OSName: 'Windows'
+ OSVmImage: 'windows-2022'
+ ProfileFlag: '-Djava-lts'
+ JavaVersion: '1.11'
+
+ pool:
+ name: $(WINDOWSPOOL)
+ image: $(WINDOWSVMIMAGE)
+ os: windows
+
+ steps:
+ - template: /eng/templates/test-steps.yml
+
+ - job:
+ displayName: 'Test'
+ strategy:
+ matrix:
+ macOS - Java 8:
+ OSName: 'macOS'
+ OSVmImage: 'macOS-13'
+ ProfileFlag: '-Djava8'
+ JavaVersion: '1.8'
+ macOS - Java LTS:
+ OSName: 'macOS'
+ OSVmImage: 'macOS-13'
+ ProfileFlag: '-Djava-lts'
+ JavaVersion: '1.11'
+
+ pool:
+ name: $(MACPOOL)
+ vmImage: $(MACVMIMAGE)
+ os: macOS
+
+ steps:
+ - template: /eng/templates/test-steps.yml
\ No newline at end of file
=====================================
debian/changelog
=====================================
@@ -1,3 +1,13 @@
+qpid-proton-j-extensions (1.2.5-1) unstable; urgency=medium
+
+ * Team upload
+ * New upstream version 1.2.5
+ * Add patch for deprecated Mockito method (Closes: #1086310)
+ * Freshen years in debian/copyright
+ * Bump Standards-Version to 4.7.0
+
+ -- tony mancill <tmancill at debian.org> Sun, 24 Nov 2024 21:02:20 -0800
+
qpid-proton-j-extensions (1.2.4-2) unstable; urgency=medium
* Team upload
=====================================
debian/control
=====================================
@@ -11,7 +11,7 @@ Build-Depends-Indep:
libapache-qpid-proton-j-java (>= 0.33.2),
libslf4j-java (>= 1.7.28),
libmockito-java <!nocheck>,
-Standards-Version: 4.6.2
+Standards-Version: 4.7.0
Vcs-Git: https://salsa.debian.org/java-team/qpid-proton-j-extensions.git
Vcs-Browser: https://salsa.debian.org/java-team/qpid-proton-j-extensions
Homepage: https://github.com/Azure/qpid-proton-j-extensions
=====================================
debian/copyright
=====================================
@@ -3,11 +3,12 @@ Upstream-Name: qpid-proton-j-extensions
Source: https://github.com/Azure/qpid-proton-j-extensions
Files: *
-Copyright: 2023, Microsoft Corporation
+Copyright: 2023-2024, Microsoft Corporation
License: MIT
Files: debian/*
-Copyright: 2023, Joseph Nahmias <jello at debian.org>
+Copyright: 2023-2024, Joseph Nahmias <jello at debian.org>
+ 2024, tony mancill <tmancill at debian.org>
License: MIT
License: MIT
=====================================
debian/patches/02-mockito-compat.patch
=====================================
@@ -0,0 +1,33 @@
+Description: Stop using deprecated Mockito.verifyZeroInteractions()
+Author: tony mancill <tmancill at debian.org>
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1086310
+
+--- a/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyAuthenticatorTests.java
++++ b/src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyAuthenticatorTests.java
+@@ -22,7 +22,7 @@
+
+ import static org.mockito.ArgumentMatchers.argThat;
+ import static org.mockito.Mockito.mock;
+-import static org.mockito.Mockito.verifyZeroInteractions;
++import static org.mockito.Mockito.verifyNoInteractions;
+ import static org.mockito.Mockito.when;
+
+ public class ProxyAuthenticatorTests {
+@@ -114,7 +114,7 @@
+ PasswordAuthentication authentication = proxyAuthenticator.getPasswordAuthentication(scheme, PROXY_ADDRESS);
+
+ // Assert
+- verifyZeroInteractions(proxySelector);
++ verifyNoInteractions(proxySelector);
+
+ Assert.assertNotNull(authentication);
+ Assert.assertEquals(configuration.credentials().getUserName(), authentication.getUserName());
+@@ -148,7 +148,7 @@
+ PasswordAuthentication authentication = proxyAuthenticator.getPasswordAuthentication(scheme, PROXY_ADDRESS);
+
+ // Assert
+- verifyZeroInteractions(proxySelector);
++ verifyNoInteractions(proxySelector);
+
+ Assert.assertNotNull(authentication);
+ Assert.assertEquals(configuration.credentials().getUserName(), authentication.getUserName());
=====================================
debian/patches/series
=====================================
@@ -1 +1,2 @@
javac-source-target.patch
+02-mockito-compat.patch
=====================================
eng/templates/1es-redirect.yml
=====================================
@@ -0,0 +1,40 @@
+resources:
+ repositories:
+ - repository: 1ESPipelineTemplates
+ type: git
+ name: 1ESPipelineTemplates/1ESPipelineTemplates
+ ref: refs/tags/release
+
+parameters:
+- name: stages
+ type: stageList
+ default: []
+- name: UseOfficial
+ type: boolean
+ default: true
+
+extends:
+ ${{ if and(parameters.UseOfficial, eq(variables['System.TeamProject'], 'internal')) }}:
+ template: v1/1ES.Official.PipelineTemplate.yml at 1ESPipelineTemplates
+ ${{ else }}:
+ template: v1/1ES.Unofficial.PipelineTemplate.yml at 1ESPipelineTemplates
+ parameters:
+ settings:
+ skipBuildTagsForGitHubPullRequests: true
+ sdl:
+ sourceAnalysisPool:
+ name: azsdk-pool-mms-win-2022-general
+ image: azsdk-pool-mms-win-2022-1espt
+ os: windows
+ sourceRepositoriesToScan:
+ exclude:
+ - repository: azure-sdk-build-tools
+ eslint:
+ enabled: false
+ justificationForDisabling: 'ESLint injected task has failures because it uses an old version of mkdirp. We should not fail for tools not controlled by the repo. See: https://dev.azure.com/azure-sdk/internal/_build/results?buildId=3499746'
+ psscriptanalyzer:
+ compiled: true
+ break: true
+ policy: M365
+
+ stages: ${{ parameters.stages }}
=====================================
eng/templates/image.yml
=====================================
@@ -0,0 +1,24 @@
+# Default pool image selection. Set as variable so we can override at pipeline level
+
+variables:
+ - name: LINUXPOOL
+ value: azsdk-pool-mms-ubuntu-2204-general
+ - name: WINDOWSPOOL
+ value: azsdk-pool-mms-win-2022-general
+ - name: MACPOOL
+ value: Azure Pipelines
+
+ - name: LINUXVMIMAGE
+ value: azsdk-pool-mms-ubuntu-2204-1espt
+ - name: WINDOWSVMIMAGE
+ value: azsdk-pool-mms-win-2022-1espt
+ - name: MACVMIMAGE
+ value: macos-13
+
+ # Values required for pool.os field in 1es pipeline templates
+ - name: LINUXOS
+ value: linux
+ - name: WINDOWSOS
+ value: windows
+ - name: MACOS
+ value: macOS
=====================================
eng/templates/test-steps.yml
=====================================
@@ -0,0 +1,19 @@
+# relies on variable settings set from calling previous build steps
+steps:
+ - task: Maven at 3
+ displayName: 'Run Tests'
+ inputs:
+ mavenPomFile: 'pom.xml'
+ options: '$(DefaultOptions) $(ProfileFlag) --settings eng/settings.xml'
+ mavenOptions: '-Xmx3072m $(LoggingOptions)'
+ javaHomeOption: 'JDKVersion'
+ jdkVersionOption: $(JavaVersion)
+ jdkArchitectureOption: 'x64'
+ publishJUnitResults: false
+ goals: 'test'
+
+ - task: PublishTestResults at 2
+ condition: succeededOrFailed()
+ inputs:
+ mergeTestResults: true
+ testRunTitle: '$(OSName) on Java $(JavaVersion)'
\ No newline at end of file
=====================================
pom.xml
=====================================
@@ -12,7 +12,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure</groupId>
<artifactId>qpid-proton-j-extensions</artifactId>
- <version>1.2.4</version>
+ <version>1.2.5</version>
<licenses>
<license>
@@ -45,7 +45,7 @@
<packageOutputDirectory>${project.build.directory}</packageOutputDirectory>
<!-- Product dependency versions -->
- <proton-j-version>0.33.8</proton-j-version>
+ <proton-j-version>0.34.1</proton-j-version>
<slf4j-version>1.7.28</slf4j-version>
<!-- Test dependency versions -->
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/HttpStatusLine.java
=====================================
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * The first line in an HTTP 1.0/1.1 response. Consists of the HTTP protocol version, status code, and a reason phrase
+ * for the HTTP response.
+ *
+ * @see <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;
+ }
+}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyHandler.java
=====================================
@@ -3,7 +3,6 @@
package com.microsoft.azure.proton.transport.proxy;
-import java.nio.ByteBuffer;
import java.util.Map;
/**
@@ -11,33 +10,6 @@ import java.util.Map;
*/
public interface ProxyHandler {
- /**
- * Represents a response from the proxy.
- */
- class ProxyResponseResult {
- private final Boolean isSuccess;
- private final String error;
-
- /**
- * Creates a new response.
- *
- * @param isSuccess {@code true} if it was successful; {@code false} otherwise.
- * @param error The error from the proxy. Or {@code null} if there was none.
- */
- public ProxyResponseResult(final Boolean isSuccess, final String error) {
- this.isSuccess = isSuccess;
- this.error = error;
- }
-
- public Boolean getIsSuccess() {
- return isSuccess;
- }
-
- public String getError() {
- return error;
- }
- }
-
/**
* Creates a CONNECT request to the provided {@code hostName} and adds {@code additionalHeaders} to the request.
*
@@ -48,11 +20,11 @@ public interface ProxyHandler {
String createProxyRequest(String hostName, Map<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);
}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyResponse.java
=====================================
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents an HTTP response from a proxy.
+ */
+public interface ProxyResponse {
+ /**
+ * Gets the headers for the HTTP response.
+ *
+ * @return The headers for the HTTP response.
+ */
+ Map<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);
+}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/impl/Constants.java
=====================================
@@ -9,7 +9,7 @@ import java.util.Locale;
* Package private constants.
*/
class Constants {
- static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate:";
+ static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
static final String DIGEST = "Digest";
@@ -18,4 +18,11 @@ class Constants {
static final String DIGEST_LOWERCASE = Constants.DIGEST.toLowerCase(Locale.ROOT);
static final String CONNECT = "CONNECT";
+
+ static final String PROXY_CONNECT_FAILED = "Proxy connect request failed with error: ";
+ static final String PROXY_CONNECT_USER_ERROR = "User configuration error. Using non-matching proxy authentication.";
+
+ static final int PROXY_HANDSHAKE_BUFFER_SIZE = 4 * 1024; // buffers used only for proxy-handshake
+
+ static final String CONTENT_LENGTH = "Content-Length";
}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImpl.java
=====================================
@@ -28,7 +28,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/
public class DigestProxyChallengeProcessorImpl implements ProxyChallengeProcessor {
static final String DEFAULT_ALGORITHM = "MD5";
- private static final String PROXY_AUTH_DIGEST = Constants.PROXY_AUTHENTICATE_HEADER + " " + Constants.DIGEST;
+ private static final String PROXY_AUTH_DIGEST = Constants.DIGEST;
private static final char[] HEX_CODE = "0123456789ABCDEF".toCharArray();
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImpl.java
=====================================
@@ -3,16 +3,19 @@
package com.microsoft.azure.proton.transport.proxy.impl;
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
import com.microsoft.azure.proton.transport.proxy.Proxy;
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
-import java.util.Scanner;
-import java.util.function.Predicate;
-import java.util.regex.Pattern;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Implementation class that handles connecting to the proxy.
@@ -27,8 +30,10 @@ public class ProxyHandlerImpl implements ProxyHandler {
static final String CONNECT_REQUEST = "CONNECT %1$s HTTP/1.1%2$sHost: %1$s%2$sConnection: Keep-Alive%2$s";
static final String HEADER_FORMAT = "%s: %s";
static final String NEW_LINE = "\r\n";
- private final Pattern successStatusLine = Pattern.compile("^http/1\\.(0|1) (?<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());
}
}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java
=====================================
@@ -8,6 +8,7 @@ import com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType;
import com.microsoft.azure.proton.transport.proxy.ProxyChallengeProcessor;
import com.microsoft.azure.proton.transport.proxy.ProxyConfiguration;
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
import org.apache.qpid.proton.engine.Transport;
import org.apache.qpid.proton.engine.TransportException;
import org.apache.qpid.proton.engine.impl.TransportImpl;
@@ -19,16 +20,23 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Scanner;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType.BASIC;
import static com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType.DIGEST;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHENTICATE;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_CONNECT_FAILED;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_CONNECT_USER_ERROR;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_HANDSHAKE_BUFFER_SIZE;
import static org.apache.qpid.proton.engine.impl.ByteBufferUtils.newWriteableBuffer;
/**
@@ -40,9 +48,6 @@ import static org.apache.qpid.proton.engine.impl.ByteBufferUtils.newWriteableBuf
*/
public class ProxyImpl implements Proxy, TransportLayer {
private static final Logger LOGGER = LoggerFactory.getLogger(ProxyImpl.class);
- private static final String PROXY_CONNECT_FAILED = "Proxy connect request failed with error: ";
- private static final String PROXY_CONNECT_USER_ERROR = "User configuration error. Using non-matching proxy authentication.";
- private static final int PROXY_HANDSHAKE_BUFFER_SIZE = 8 * 1024; // buffers used only for proxy-handshake
private final ByteBuffer inputBuffer;
private final ByteBuffer outputBuffer;
@@ -50,28 +55,27 @@ public class ProxyImpl implements Proxy, TransportLayer {
private boolean tailClosed = false;
private boolean headClosed = false;
- private boolean isProxyConfigured;
private String host = "";
private Map<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<>();
-
- try (Scanner scanner = new Scanner(error)) {
- while (scanner.hasNextLine()) {
- String line = scanner.nextLine().trim();
+ final Set<ProxyAuthenticationType> supportedTypes = new HashSet<>();
+ final List<String> authenticationTypes = headers.get(PROXY_AUTHENTICATE);
- if (!line.startsWith(Constants.PROXY_AUTHENTICATE_HEADER)) {
- continue;
- }
-
- String substring = line.substring(Constants.PROXY_AUTHENTICATE_HEADER.length())
- .trim().toLowerCase(Locale.ROOT);
+ for (String type : authenticationTypes) {
+ final String lowercase = type.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();
+ }
}
}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImpl.java
=====================================
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy.impl;
+
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
+import com.microsoft.azure.proton.transport.ws.WebSocket.WebSocketFrameReadState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.CONTENT_LENGTH;
+
+/**
+ * Represents an HTTP response from a proxy.
+ *
+ * @see <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);
+ }
+
+}
=====================================
src/main/java/com/microsoft/azure/proton/transport/proxy/impl/StringUtils.java
=====================================
@@ -7,6 +7,8 @@ package com.microsoft.azure.proton.transport.proxy.impl;
* Utility classes for strings.
*/
class StringUtils {
+ static final String NEW_LINE = "\r\n";
+
static boolean isNullOrEmpty(String string) {
return string == null || string.isEmpty();
}
=====================================
src/test/java/com/microsoft/azure/proton/transport/proxy/impl/DigestProxyChallengeProcessorImplTest.java
=====================================
@@ -180,10 +180,10 @@ public class DigestProxyChallengeProcessorImplTest {
}
private static String generateProxyChallenge(String realm, String nonce, String qop) {
- final String digest = String.format("%s %s realm=\"%s\", nonce=\"%s\", qop=\"%s\", stale=false",
- Constants.PROXY_AUTHENTICATE_HEADER, Constants.DIGEST, realm, nonce, qop);
- final String basic = String.format("%s %s realm=\"%s\"",
- Constants.PROXY_AUTHENTICATE_HEADER, Constants.BASIC, realm);
+ final String digest = String.format("%s realm=\"%s\", nonce=\"%s\", qop=\"%s\", stale=false",
+ Constants.DIGEST, realm, nonce, qop);
+ final String basic = String.format("%s realm=\"%s\"",
+ Constants.BASIC, realm);
return String.join(NEW_LINE,
"HTTP/1.1 407 Proxy Authentication Required",
=====================================
src/test/java/com/microsoft/azure/proton/transport/proxy/impl/HttpStatusLineTest.java
=====================================
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.proton.transport.proxy.impl;
+
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class HttpStatusLineTest {
+
+ /**
+ * Verifies that it successfully parses a valid HTTP status line.
+ */
+ @Test
+ public void validStatusLine() {
+ // Arrange
+ final String line = "HTTP/1.1 200 Connection Established";
+
+ // Act
+ final HttpStatusLine actual = HttpStatusLine.create(line);
+
+ // Assert
+ Assert.assertNotNull(actual);
+
+ Assert.assertEquals(200, actual.getStatusCode());
+ Assert.assertEquals("1.1", actual.getProtocolVersion());
+ Assert.assertEquals("Connection Established", actual.getReason());
+ }
+
+
+ /**
+ * Verifies that status line length is invalid
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"HTTP/1.1 InvalidLength", "HTTP/1.1 Invalid Code", "HTTP/invalid protocol"})
+ public void invalidStatusLine(String line) {
+
+ // Act & Assert
+ Assert.assertThrows(IllegalArgumentException.class, () -> HttpStatusLine.create(line));
+ }
+
+
+}
=====================================
src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyHandlerImplTest.java
=====================================
@@ -3,16 +3,19 @@
package com.microsoft.azure.proton.transport.proxy.impl;
-import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
+import com.microsoft.azure.proton.transport.proxy.HttpStatusLine;
+import com.microsoft.azure.proton.transport.proxy.ProxyResponse;
import org.junit.Assert;
import org.junit.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import java.util.HashMap;
+import static com.microsoft.azure.proton.transport.proxy.impl.StringUtils.NEW_LINE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
public class ProxyHandlerImplTest {
@Test
public void testCreateProxyRequest() {
@@ -34,79 +37,63 @@ public class ProxyHandlerImplTest {
Assert.assertEquals(expectedProxyRequest, actualProxyRequest);
}
- @ParameterizedTest
- @ValueSource(ints = {200, 201, 202, 203, 204, 205, 206})
- public void testValidateProxyResponseOnSuccess(int httpCode) {
- final String validResponse = "HTTP/1.1 " + httpCode + "Connection Established\r\n"
- + "FiddlerGateway: Direct\r\n"
- + "StartTime: 13:08:21.574\r\n"
- + "Connection: close";
- final ByteBuffer buffer = ByteBuffer.allocate(1024);
- buffer.put(validResponse.getBytes(StandardCharsets.UTF_8));
- buffer.flip();
-
- final ProxyHandlerImpl proxyHandler = new ProxyHandlerImpl();
- ProxyHandler.ProxyResponseResult responseResult = proxyHandler.validateProxyResponse(buffer);
-
- Assert.assertTrue(responseResult.getIsSuccess());
- Assert.assertNull(responseResult.getError());
-
- 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();
-
+ 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.assertEquals(failResponse, responseResult.getError());
+ // Act
+ final boolean result = proxyHandler.validateProxyResponse(response);
+
+ // Assert
+ Assert.assertTrue(result);
- 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();
+ public void testValidateProxyResponseOnFailure() {
+ // 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(invalidResponse, responseResult.getError());
+ // Act
+ final boolean result = proxyHandler.validateProxyResponse(response);
- 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);
}
}
=====================================
src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImplTest.java
=====================================
@@ -18,6 +18,9 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,10 +39,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import static com.microsoft.azure.proton.transport.proxy.impl.Constants.BASIC;
import static com.microsoft.azure.proton.transport.proxy.impl.Constants.DIGEST;
-import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHENTICATE_HEADER;
+import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHENTICATE;
import static com.microsoft.azure.proton.transport.proxy.impl.Constants.PROXY_AUTHORIZATION;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
@@ -59,14 +63,20 @@ import static org.mockito.Mockito.when;
public class ProxyImplTest {
private static final InetSocketAddress PROXY_ADDRESS = InetSocketAddress.createUnresolved("my.host.name", 8888);
private static final java.net.Proxy PROXY = new java.net.Proxy(java.net.Proxy.Type.HTTP, PROXY_ADDRESS);
- private static final int BUFFER_SIZE = 8 * 1024;
+ private static final int BUFFER_SIZE = Constants.PROXY_HANDSHAKE_BUFFER_SIZE;
private static final String USERNAME = "test-user";
private static final String PASSWORD = "test-password!";
+ private static final String BASIC_HEADER = BASIC;
+ private static final String DIGEST_HEADER = String.format("%s realm=\"%s\", nonce=\"A randomly set nonce.\", qop=\"auth\", stale=false",
+ DIGEST, PROXY);
private final Logger logger = LoggerFactory.getLogger(ProxyImplTest.class);
private final Map<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);
+ }
+ }
+ }
+
+ private String getProxyResponse(String[] statusLine, List<String> authentications) {
+ final Map<String, List<String>> headers = new HashMap<>();
+
+ if (!authentications.isEmpty()) {
+ headers.put(PROXY_AUTHENTICATE, authentications);
}
- 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);
+ 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);
}
- builder.append(newLine);
+ String response = TestUtils.createProxyResponse(statusLine, headers, body);
- return builder.toString();
+ //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;
}
+
+
}
=====================================
src/test/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyResponseImplTest.java
=====================================
@@ -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());
+ }
+}
=====================================
src/test/java/com/microsoft/azure/proton/transport/proxy/impl/TestUtils.java
=====================================
@@ -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;
+ }
+
+}
View it on GitLab: https://salsa.debian.org/java-team/qpid-proton-j-extensions/-/compare/4277ae136e5ccb537b0199fb83cf2afe0122c019...842738d275db8670660ab9a6865e7aae052406b2
--
View it on GitLab: https://salsa.debian.org/java-team/qpid-proton-j-extensions/-/compare/4277ae136e5ccb537b0199fb83cf2afe0122c019...842738d275db8670660ab9a6865e7aae052406b2
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20241125/e74dcc92/attachment.htm>
More information about the pkg-java-commits
mailing list