diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 75551894c9..fed9064c77 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -84,7 +84,7 @@ jobs: # and modify them (or add more) to build your code if your project # uses a compiled language - - run: mvn clean package -DskipTests -Drat.skip=true -Dcheckstyle.skip + - run: ./mvnw clean package -DskipTests -Drat.skip=true -Dcheckstyle.skip - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 187832dab4..ae2082fe0d 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -49,4 +49,4 @@ jobs: distribution: 'temurin' java-version: ${{ matrix.java }} - name: Build with Maven - run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -P-use-toolchains + run: ./mvnw -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false -P-use-toolchains,docker diff --git a/.gitignore b/.gitignore index cde3cfff4f..e6786cd55c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ maven-eclipse.xml *.iml **/log4j2-debug.xml **/.checkstyle +/test-CA/newcerts/ +/test-CA/serial.txt* +/test-CA/index.txt* diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..7ac4806a75 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionSha256Sum=0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip +wrapperVersion=3.3.2 diff --git a/README.md b/README.md index 39809a5a30..dca334d010 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,16 @@ Dependencies HttpCore requires Java 1.8 compatible runtime. +Protocol conformance +-------------------- +- +- [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110) - HTTP Semantics +- [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112) - Hypertext Transfer Protocol Version 1.1 (HTTP/1.1) +- [RFC 7540](https://datatracker.ietf.org/doc/html/rfc7540) - Hypertext Transfer Protocol Version 2 (HTTP/2) +- [RFC 7541](https://datatracker.ietf.org/doc/html/rfc7541) - HPACK: Header Compression for HTTP/2 +- [RFC 1945](https://datatracker.ietf.org/doc/html/rfc1945) - Hypertext Transfer Protocol -- HTTP/1.0 +- [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) - Uniform Resource Identifier (URI): Generic Syntax + Licensing --------- diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index 13d88258ef..ccec169f20 100644 --- a/RELEASE_NOTES.txt +++ b/RELEASE_NOTES.txt @@ -1,3 +1,30 @@ +Release 5.4-alpha1 +------------------ + +This is the first release in the 5.4 release series. + + +Change Log +------------------- + +* Add ContentType.APPLICATION_ZIP_COMPRESSED. + Contributed by Gary Gregory + +* Bump io.reactivex.rxjava3:rxjava from 3.1.9 to 3.1.10 #509 + Contributed by Gary Gregory + +* Bump org.junit:junit-bom from 5.11.0 to 5.13.3 #501, #528, #533 + Contributed by Gary Gregory + +* Bump testcontainers.version from 1.20.2 to 1.20.4 #505 + Contributed by Gary Gregory + +* Bump testcontainers.version from 1.21.1 to 1.21.3 #530, #534 + Contributed by Gary Gregory + +* Bump log4j.version from 2.24.3 to 2.25.0 #529 + Contributed by Gary Gregory + Release 5.3 ------------------ diff --git a/SECURITY.md b/SECURITY.md index ec123bd5fd..bc438605ec 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,7 +17,8 @@ under the License. --> -# Reporting a vulnerability +# HttpComponents Security -If you believe you found a vulnerability in Apache HttpClient, -please contact [the Apache Security Team](https://www.apache.org/security/). +The HttpComponents Security process and model are on our website's [Security](https://hc.apache.org/security.html) page. + +If you believe you found a vulnerability in Apache HttpClient, please contact the [Apache Security Team](https://www.apache.org/security/). diff --git a/httpcore5-h2/pom.xml b/httpcore5-h2/pom.xml index 5a44f9c746..fd3471aa49 100644 --- a/httpcore5-h2/pom.xml +++ b/httpcore5-h2/pom.xml @@ -28,7 +28,7 @@ org.apache.httpcomponents.core5 httpcore5-parent - 5.3.1-SNAPSHOT + 5.4-alpha1-SNAPSHOT httpcore5-h2 Apache HttpComponents Core HTTP/2 diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoRequestHeaders.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoRequestHeaders.java index 249322853b..0be8efc6dd 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoRequestHeaders.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoRequestHeaders.java @@ -34,9 +34,9 @@ */ public final class H2PseudoRequestHeaders { - public static final String METHOD = ":method"; - public static final String SCHEME = ":scheme"; + public static final String METHOD = ":method"; + public static final String SCHEME = ":scheme"; public static final String AUTHORITY = ":authority"; - public static final String PATH = ":path"; + public static final String PATH = ":path"; } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoResponseHeaders.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoResponseHeaders.java index 9ea769e628..b1de65b775 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoResponseHeaders.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/H2PseudoResponseHeaders.java @@ -34,6 +34,6 @@ */ public final class H2PseudoResponseHeaders { - public static final String STATUS = ":status"; + public static final String STATUS = ":status"; } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java index 0b28d9a708..a801cc51fe 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java @@ -50,10 +50,11 @@ public class H2Config { private final int maxFrameSize; private final int maxHeaderListSize; private final boolean compressionEnabled; + private final int maxContinuations; H2Config(final int headerTableSize, final boolean pushEnabled, final int maxConcurrentStreams, final int initialWindowSize, final int maxFrameSize, final int maxHeaderListSize, - final boolean compressionEnabled) { + final boolean compressionEnabled, final int maxContinuations) { super(); this.headerTableSize = headerTableSize; this.pushEnabled = pushEnabled; @@ -62,6 +63,7 @@ public class H2Config { this.maxFrameSize = maxFrameSize; this.maxHeaderListSize = maxHeaderListSize; this.compressionEnabled = compressionEnabled; + this.maxContinuations = maxContinuations; } public int getHeaderTableSize() { @@ -92,6 +94,10 @@ public boolean isCompressionEnabled() { return compressionEnabled; } + public int getMaxContinuations() { + return maxContinuations; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -102,6 +108,7 @@ public String toString() { .append(", maxFrameSize=").append(this.maxFrameSize) .append(", maxHeaderListSize=").append(this.maxHeaderListSize) .append(", compressionEnabled=").append(this.compressionEnabled) + .append(", maxContinuations=").append(this.maxContinuations) .append("]"); return builder.toString(); } @@ -110,11 +117,11 @@ public static H2Config.Builder custom() { return new Builder(); } - private static final int INIT_HEADER_TABLE_SIZE = 4096; - private static final boolean INIT_ENABLE_PUSH = true; - private static final int INIT_MAX_FRAME_SIZE = FrameConsts.MIN_FRAME_SIZE; - private static final int INIT_WINDOW_SIZE = 65535; - private static final int INIT_CONCURRENT_STREAM = 250; + private static final int INIT_HEADER_TABLE_SIZE = 4096; + private static final boolean INIT_ENABLE_PUSH = true; + private static final int INIT_MAX_FRAME_SIZE = FrameConsts.MIN_FRAME_SIZE; + private static final int INIT_WINDOW_SIZE = 65535; + private static final int INIT_CONCURRENT_STREAM = 250; public static H2Config.Builder initial() { return new Builder() @@ -147,15 +154,17 @@ public static class Builder { private int maxFrameSize; private int maxHeaderListSize; private boolean compressionEnabled; + private int maxContinuations; Builder() { this.headerTableSize = INIT_HEADER_TABLE_SIZE * 2; this.pushEnabled = INIT_ENABLE_PUSH; this.maxConcurrentStreams = INIT_CONCURRENT_STREAM; this.initialWindowSize = INIT_WINDOW_SIZE; - this.maxFrameSize = FrameConsts.MIN_FRAME_SIZE * 4; + this.maxFrameSize = FrameConsts.MIN_FRAME_SIZE * 4; this.maxHeaderListSize = FrameConsts.MAX_FRAME_SIZE; this.compressionEnabled = true; + this.maxContinuations = 100; } public Builder setHeaderTableSize(final int headerTableSize) { @@ -198,6 +207,18 @@ public Builder setCompressionEnabled(final boolean compressionEnabled) { return this; } + /** + * Sets max limit on number of continuations. + *

value zero represents no limit

+ * + * @since 5,4 + */ + public Builder setMaxContinuations(final int maxContinuations) { + Args.positive(maxContinuations, "Max continuations"); + this.maxContinuations = maxContinuations; + return this; + } + public H2Config build() { return new H2Config( headerTableSize, @@ -206,7 +227,8 @@ public H2Config build() { initialWindowSize, maxFrameSize, maxHeaderListSize, - compressionEnabled); + compressionEnabled, + maxContinuations); } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Param.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Param.java index 5f9604972d..7d4455e22f 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Param.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Param.java @@ -33,12 +33,12 @@ */ public enum H2Param { - HEADER_TABLE_SIZE (0x1), - ENABLE_PUSH (0x2), - MAX_CONCURRENT_STREAMS (0x3), - INITIAL_WINDOW_SIZE (0x4), - MAX_FRAME_SIZE (0x5), - MAX_HEADER_LIST_SIZE (0x6); + HEADER_TABLE_SIZE(0x1), + ENABLE_PUSH(0x2), + MAX_CONCURRENT_STREAMS(0x3), + INITIAL_WINDOW_SIZE(0x4), + MAX_FRAME_SIZE(0x5), + MAX_HEADER_LIST_SIZE(0x6); int code; diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/DefaultFrameFactory.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/DefaultFrameFactory.java index 77e3be66fe..deb4eb7df7 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/DefaultFrameFactory.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/DefaultFrameFactory.java @@ -50,21 +50,21 @@ public RawFrame createHeaders(final int streamId, final ByteBuffer payload, fina @Override public RawFrame createContinuation(final int streamId, final ByteBuffer payload, final boolean endHeaders) { Args.positive(streamId, "Stream id"); - final int flags = (endHeaders ? FrameFlag.END_HEADERS.value : 0); + final int flags = endHeaders ? FrameFlag.END_HEADERS.value : 0; return new RawFrame(FrameType.CONTINUATION.getValue(), flags, streamId, payload); } @Override public RawFrame createPushPromise(final int streamId, final ByteBuffer payload, final boolean endHeaders) { Args.positive(streamId, "Stream id"); - final int flags = (endHeaders ? FrameFlag.END_HEADERS.value : 0); + final int flags = endHeaders ? FrameFlag.END_HEADERS.value : 0; return new RawFrame(FrameType.PUSH_PROMISE.getValue(), flags, streamId, payload); } @Override public RawFrame createData(final int streamId, final ByteBuffer payload, final boolean endStream) { Args.positive(streamId, "Stream id"); - final int flags = (endStream ? FrameFlag.END_STREAM.value : 0); + final int flags = endStream ? FrameFlag.END_STREAM.value : 0; return new RawFrame(FrameType.DATA.getValue(), flags, streamId, payload); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameFlag.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameFlag.java index 9e480fbf11..d21381b29d 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameFlag.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameFlag.java @@ -33,11 +33,11 @@ */ public enum FrameFlag { - END_STREAM (0x01), - ACK (0x01), - END_HEADERS (0x04), - PADDED (0x08), - PRIORITY (0x20); + END_STREAM(0x01), + ACK(0x01), + END_HEADERS(0x04), + PADDED(0x08), + PRIORITY(0x20); final int value; diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameType.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameType.java index 3f9cc3152b..483af68a6c 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameType.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameType.java @@ -33,16 +33,16 @@ */ public enum FrameType { - DATA (0x00), - HEADERS (0x01), - PRIORITY (0x02), - RST_STREAM (0x03), - SETTINGS (0x04), - PUSH_PROMISE (0x05), - PING (0x06), - GOAWAY (0x07), - WINDOW_UPDATE (0x08), - CONTINUATION (0x09); + DATA(0x00), + HEADERS(0x01), + PRIORITY(0x02), + RST_STREAM(0x03), + SETTINGS(0x04), + PUSH_PROMISE(0x05), + PING(0x06), + GOAWAY(0x07), + WINDOW_UPDATE(0x08), + CONTINUATION(0x09); int value; diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java index 880d0ccbde..855430466f 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java @@ -36,6 +36,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.http.Header; @@ -62,10 +63,10 @@ public final class HPackDecoder { private int maxListSize; HPackDecoder(final InboundDynamicTable dynamicTable, final CharsetDecoder charsetDecoder) { - this.dynamicTable = dynamicTable != null ? dynamicTable : new InboundDynamicTable(); + this.dynamicTable = Objects.requireNonNull(dynamicTable); this.contentBuf = new ByteArrayBuffer(256); this.charsetDecoder = charsetDecoder; - this.maxTableSize = dynamicTable != null ? dynamicTable.getMaxSize() : Integer.MAX_VALUE; + this.maxTableSize = this.dynamicTable.getMaxSize(); this.maxListSize = Integer.MAX_VALUE; } @@ -73,12 +74,12 @@ public final class HPackDecoder { this(dynamicTable, charset != null && !StandardCharsets.US_ASCII.equals(charset) ? charset.newDecoder() : null); } - public HPackDecoder(final Charset charset) { - this(new InboundDynamicTable(), charset); + public HPackDecoder(final int maxTableSize, final Charset charset) { + this(new InboundDynamicTable(maxTableSize), charset); } - public HPackDecoder(final CharsetDecoder charsetDecoder) { - this(new InboundDynamicTable(), charsetDecoder); + public HPackDecoder(final int maxTableSize, final CharsetDecoder charsetDecoder) { + this(new InboundDynamicTable(maxTableSize), charsetDecoder); } static int readByte(final ByteBuffer src) throws HPackException { @@ -238,7 +239,7 @@ HPackHeader decodeLiteralHeader( nameLen = decodeString(src, buf); name = buf.toString(); } else { - final HPackHeader existing = this.dynamicTable.getHeader(index); + final HPackHeader existing = this.dynamicTable.getHeader(index); if (existing == null) { throw new HPackException("Invalid header index"); } @@ -258,7 +259,7 @@ HPackHeader decodeLiteralHeader( HPackHeader decodeIndexedHeader(final ByteBuffer src) throws HPackException { final int index = decodeInt(src, 7); - final HPackHeader existing = this.dynamicTable.getHeader(index); + final HPackHeader existing = this.dynamicTable.getHeader(index); if (existing == null) { throw new HPackException("Invalid header index"); } @@ -284,7 +285,10 @@ HPackHeader decodeHPackHeader(final ByteBuffer src) throws HPackException { return decodeLiteralHeader(src, HPackRepresentation.NEVER_INDEXED); } else if ((b & 0xe0) == 0x20) { final int maxSize = decodeInt(src, 5); - this.dynamicTable.setMaxSize(Math.min(this.maxTableSize, maxSize)); + if (maxSize > this.maxTableSize) { + throw new HPackException("Requested dynamic header table size exceeds maximum size: " + maxSize); + } + this.dynamicTable.setMaxSize(maxSize); } else { throw new HPackException("Unexpected header first byte: 0x" + Integer.toHexString(b)); } @@ -323,7 +327,7 @@ public int getMaxTableSize() { public void setMaxTableSize(final int maxTableSize) { Args.notNegative(maxTableSize, "Max table size"); this.maxTableSize = maxTableSize; - this.dynamicTable.setMaxSize(maxTableSize); + this.dynamicTable.setMaxSize(Math.min(this.dynamicTable.getMaxSize(), maxTableSize)); } public int getMaxListSize() { diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java index b64b27a51e..a06c5c9d00 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java @@ -57,21 +57,22 @@ public final class HPackEncoder { private int maxTableSize; HPackEncoder(final OutboundDynamicTable dynamicTable, final CharsetEncoder charsetEncoder) { - this.dynamicTable = dynamicTable != null ? dynamicTable : new OutboundDynamicTable(); + this.dynamicTable = Objects.requireNonNull(dynamicTable); this.huffmanBuf = new ByteArrayBuffer(128); this.charsetEncoder = charsetEncoder; + this.maxTableSize = this.dynamicTable.getMaxSize(); } HPackEncoder(final OutboundDynamicTable dynamicTable, final Charset charset) { this(dynamicTable, charset != null && !StandardCharsets.US_ASCII.equals(charset) ? charset.newEncoder() : null); } - public HPackEncoder(final Charset charset) { - this(new OutboundDynamicTable(), charset); + public HPackEncoder(final int maxTableSize, final Charset charset) { + this(new OutboundDynamicTable(maxTableSize), charset); } - public HPackEncoder(final CharsetEncoder charsetEncoder) { - this(new OutboundDynamicTable(), charsetEncoder); + public HPackEncoder(final int maxTableSize, final CharsetEncoder charsetEncoder) { + this(new OutboundDynamicTable(maxTableSize), charsetEncoder); } static void encodeInt(final ByteArrayBuffer dst, final int n, final int i, final int mask) { @@ -261,6 +262,11 @@ void encodeHeader( void encodeHeader( final ByteArrayBuffer dst, final String name, final String value, final boolean sensitive, final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException { + //send receiver the updated dynamic table size + if (maxTableSize != this.dynamicTable.getMaxSize()) { + encodeInt(dst, 5, maxTableSize, 0x20); + this.dynamicTable.setMaxSize(maxTableSize); + } final HPackRepresentation representation; if (sensitive) { @@ -336,7 +342,6 @@ public int getMaxTableSize() { public void setMaxTableSize(final int maxTableSize) { Args.notNegative(maxTableSize, "Max table size"); this.maxTableSize = maxTableSize; - this.dynamicTable.setMaxSize(maxTableSize); } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HuffmanEncoder.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HuffmanEncoder.java index 017033976c..2ffb547c18 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HuffmanEncoder.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HuffmanEncoder.java @@ -66,8 +66,8 @@ void encode(final ByteArrayBuffer out, final ByteBuffer src) { } if (n > 0) { - current <<= (8 - n); - current |= (0xFF >>> n); // this should be EOS symbol + current <<= 8 - n; + current |= 0xFF >>> n; // this should be EOS symbol out.append((int) current); } } @@ -93,8 +93,8 @@ void encode(final ByteArrayBuffer out, final CharSequence src, final int off, fi } if (n > 0) { - current <<= (8 - n); - current |= (0xFF >>> n); // this should be EOS symbol + current <<= 8 - n; + current |= 0xFF >>> n; // this should be EOS symbol out.append((int) current); } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/InboundDynamicTable.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/InboundDynamicTable.java index 28c3f0b723..88e10f0850 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/InboundDynamicTable.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/InboundDynamicTable.java @@ -39,15 +39,19 @@ final class InboundDynamicTable { private int maxSize; private int currentSize; - InboundDynamicTable(final StaticTable staticTable) { + InboundDynamicTable(final int maxSize, final StaticTable staticTable) { this.staticTable = staticTable; this.headers = new FifoBuffer(256); - this.maxSize = Integer.MAX_VALUE; + this.maxSize = maxSize; this.currentSize = 0; } + InboundDynamicTable(final int maxSize) { + this(maxSize, StaticTable.INSTANCE); + } + InboundDynamicTable() { - this(StaticTable.INSTANCE); + this(Integer.MAX_VALUE); } public int getMaxSize() { diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/OutboundDynamicTable.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/OutboundDynamicTable.java index 3791b48afd..92aed4659c 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/OutboundDynamicTable.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/OutboundDynamicTable.java @@ -45,16 +45,20 @@ final class OutboundDynamicTable { private int maxSize; private int currentSize; - OutboundDynamicTable(final StaticTable staticTable) { + OutboundDynamicTable(final int maxSize, final StaticTable staticTable) { this.staticTable = staticTable; this.headers = new FifoLinkedList(); this.mapByName = new HashMap<>(); - this.maxSize = Integer.MAX_VALUE; + this.maxSize = maxSize; this.currentSize = 0; } + OutboundDynamicTable(final int maxSize) { + this(maxSize, StaticTable.INSTANCE); + } + OutboundDynamicTable() { - this(StaticTable.INSTANCE); + this(Integer.MAX_VALUE); } public int getMaxSize() { diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/StaticTable.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/StaticTable.java index 91b3db39a9..c58ee50433 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/StaticTable.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/StaticTable.java @@ -112,12 +112,10 @@ final class StaticTable { for (int i = 0; i < headers.length; i++) { final HPackHeader header = headers[i]; - final String key = header.getName(); - CopyOnWriteArrayList entries = this.mapByName.get(key); + final CopyOnWriteArrayList entries = this.mapByName.get(key); if (entries == null) { - entries = new CopyOnWriteArrayList<>(new HPackEntry[] { new InternalEntry(header, i) }); - this.mapByName.put(key, entries); + this.mapByName.put(key, new CopyOnWriteArrayList<>(new HPackEntry[] { new InternalEntry(header, i) })); } else { entries.add(new InternalEntry(header, i)); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2RequestConverter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2RequestConverter.java index c1ad90ca55..253b157269 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2RequestConverter.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2RequestConverter.java @@ -34,7 +34,6 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.Method; @@ -69,14 +68,10 @@ public HttpRequest convert(final List
headers) throws HttpException { final String name = header.getName(); final String value = header.getValue(); - for (int n = 0; n < name.length(); n++) { - final char ch = name.charAt(n); - if (Character.isAlphabetic(ch) && !Character.isLowerCase(ch)) { - throw new ProtocolException("Header name '%s' is invalid (header name contains uppercase characters)", name); - } - } - if (name.startsWith(":")) { + if (!FieldValidationSupport.isNameLowerCaseValid(name, 1, name.length())) { + throw new ProtocolException("Header name '%s' is invalid", name); + } if (!messageHeaders.isEmpty()) { throw new ProtocolException("Invalid sequence of headers (pseudo-headers must precede message headers)"); } @@ -107,16 +102,14 @@ public HttpRequest convert(final List
headers) throws HttpException { throw new ProtocolException("Unsupported request header '%s'", name); } } else { - if (name.equalsIgnoreCase(HttpHeaders.CONNECTION) || name.equalsIgnoreCase(HttpHeaders.KEEP_ALIVE) - || name.equalsIgnoreCase(HttpHeaders.PROXY_CONNECTION) || name.equalsIgnoreCase(HttpHeaders.TRANSFER_ENCODING) - || name.equalsIgnoreCase(HttpHeaders.HOST) || name.equalsIgnoreCase(HttpHeaders.UPGRADE)) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue()); - } - if (name.equalsIgnoreCase(HttpHeaders.TE) && !value.equalsIgnoreCase("trailers")) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue()); + if (!FieldValidationSupport.isNameLowerCaseValid(name)) { + throw new ProtocolException("Header name '%s' is invalid", name); } messageHeaders.add(header); } + if (!FieldValidationSupport.isValueValid(value)) { + throw new ProtocolException("Header value is invalid"); + } } if (method == null) { throw new ProtocolException("Mandatory request header '%s' not found", H2PseudoRequestHeaders.METHOD); @@ -181,7 +174,7 @@ public List
convert(final HttpRequest message) throws HttpException { headers.add(new BasicHeader(H2PseudoRequestHeaders.METHOD, message.getMethod(), false)); if (optionMethod) { headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false)); - } else { + } else { headers.add(new BasicHeader(H2PseudoRequestHeaders.SCHEME, message.getScheme(), false)); if (message.getAuthority() != null) { headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false)); @@ -193,16 +186,11 @@ public List
convert(final HttpRequest message) throws HttpException { final Header header = it.next(); final String name = header.getName(); final String value = header.getValue(); - if (name.startsWith(":")) { + if (!FieldValidationSupport.isNameValid(name)) { throw new ProtocolException("Header name '%s' is invalid", name); } - if (name.equalsIgnoreCase(HttpHeaders.CONNECTION) || name.equalsIgnoreCase(HttpHeaders.KEEP_ALIVE) - || name.equalsIgnoreCase(HttpHeaders.PROXY_CONNECTION) || name.equalsIgnoreCase(HttpHeaders.TRANSFER_ENCODING) - || name.equalsIgnoreCase(HttpHeaders.HOST) || name.equalsIgnoreCase(HttpHeaders.UPGRADE)) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue()); - } - if (name.equalsIgnoreCase(HttpHeaders.TE) && !value.equalsIgnoreCase("trailers")) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue()); + if (!FieldValidationSupport.isValueValid(value)) { + throw new ProtocolException("Header value is invalid"); } headers.add(new BasicHeader(TextUtils.toLowerCase(name), value)); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2ResponseConverter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2ResponseConverter.java index 0b860be8b5..f56d0d11a5 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2ResponseConverter.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/DefaultH2ResponseConverter.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Locale; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; @@ -57,19 +58,17 @@ public HttpResponse convert(final List
headers) throws HttpException { String statusText = null; final List
messageHeaders = new ArrayList<>(); + int cookieCount = 0; + for (int i = 0; i < headers.size(); i++) { final Header header = headers.get(i); final String name = header.getName(); final String value = header.getValue(); - for (int n = 0; n < name.length(); n++) { - final char ch = name.charAt(n); - if (Character.isAlphabetic(ch) && !Character.isLowerCase(ch)) { - throw new ProtocolException("Header name '%s' is invalid (header name contains uppercase characters)", name); - } - } - if (name.startsWith(":")) { + if (!FieldValidationSupport.isNameLowerCaseValid(name, 1, name.length())) { + throw new ProtocolException("Header name '%s' is invalid", name); + } if (!messageHeaders.isEmpty()) { throw new ProtocolException("Invalid sequence of headers (pseudo-headers must precede message headers)"); } @@ -82,13 +81,17 @@ public HttpResponse convert(final List
headers) throws HttpException { throw new ProtocolException("Unsupported response header '%s'", name); } } else { - if (name.equalsIgnoreCase(HttpHeaders.CONNECTION) || name.equalsIgnoreCase(HttpHeaders.KEEP_ALIVE) - || name.equalsIgnoreCase(HttpHeaders.TRANSFER_ENCODING) || name.equalsIgnoreCase(HttpHeaders.UPGRADE)) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue()); + if (!FieldValidationSupport.isNameLowerCaseValid(name)) { + throw new ProtocolException("Header name '%s' is invalid", name); + } + if (name.equalsIgnoreCase(HttpHeaders.COOKIE)) { + cookieCount++; } messageHeaders.add(header); } - + if (!FieldValidationSupport.isValueValid(value)) { + throw new ProtocolException("Header value is invalid"); + } } if (statusText == null) { @@ -105,6 +108,20 @@ public HttpResponse convert(final List
headers) throws HttpException { for (int i = 0; i < messageHeaders.size(); i++) { response.addHeader(messageHeaders.get(i)); } + + if (cookieCount > 1) { + final StringBuilder buf = new StringBuilder(); + for (final Iterator
it = response.headerIterator(HttpHeaders.COOKIE); it.hasNext(); ) { + if (buf.length() > 0) { + buf.append("; "); + } + final Header cookie = it.next(); + buf.append(cookie.getValue()); + it.remove(); + } + response.setHeader(HttpHeaders.COOKIE.toLowerCase(Locale.ROOT), buf.toString()); + } + return response; } @@ -121,12 +138,11 @@ public List
convert(final HttpResponse message) throws HttpException { final Header header = it.next(); final String name = header.getName(); final String value = header.getValue(); - if (name.startsWith(":")) { + if (!FieldValidationSupport.isNameValid(name)) { throw new ProtocolException("Header name '%s' is invalid", name); } - if (name.equalsIgnoreCase(HttpHeaders.CONNECTION) || name.equalsIgnoreCase(HttpHeaders.KEEP_ALIVE) - || name.equalsIgnoreCase(HttpHeaders.TRANSFER_ENCODING) || name.equalsIgnoreCase(HttpHeaders.UPGRADE)) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue()); + if (!FieldValidationSupport.isValueValid(value)) { + throw new ProtocolException("Header value is invalid"); } headers.add(new BasicHeader(TextUtils.toLowerCase(name), value)); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/FieldValidationSupport.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/FieldValidationSupport.java new file mode 100644 index 0000000000..e67b6892d2 --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/FieldValidationSupport.java @@ -0,0 +1,92 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.impl; + +import org.apache.hc.core5.annotation.Internal; + +@Internal +public class FieldValidationSupport { + + public static boolean isWhitespace(final char ch) { + return ch == 0x20 || ch == 0x09; + } + + public static boolean isNameCharValid(final char ch) { + return ch > 0x20 && ch != ':' && ch < 0x7f; + } + + public static boolean isNameCharLowerCaseValid(final char ch) { + return isNameCharValid(ch) && (ch < 0x41 || ch > 0x5a); + } + + public static boolean isValueCharValid(final char ch) { + return ch != 0x00 && ch != 0x0a && ch != 0x0d; + } + + public static boolean isNameValid(final CharSequence s, final int pos, final int len) { + for (int i = pos; i < len; i++) { + if (!isNameCharValid(s.charAt(i))) { + return false; + } + } + return true; + } + + public static boolean isNameValid(final CharSequence s) { + return isNameValid(s, 0, s.length()); + } + + public static boolean isNameLowerCaseValid(final CharSequence s, final int pos, final int len) { + for (int i = pos; i < len; i++) { + if (!isNameCharLowerCaseValid(s.charAt(i))) { + return false; + } + } + return true; + } + + public static boolean isNameLowerCaseValid(final CharSequence s) { + return isNameLowerCaseValid(s, 0, s.length()); + } + + public static boolean isValueValid(final CharSequence s) { + if (s.length() == 0) { + return true; + } + if (isWhitespace(s.charAt(0)) || isWhitespace(s.charAt(s.length() - 1))) { + return false; + } + for (int i = 0; i < s.length(); i++) { + if (!isValueCharValid(s.charAt(i))) { + return false; + } + } + return true; + } + +} diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java index 5a82d83a05..b6cf55270d 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java @@ -35,10 +35,12 @@ import org.apache.hc.core5.http.protocol.ResponseConformance; import org.apache.hc.core5.http.protocol.ResponseDate; import org.apache.hc.core5.http.protocol.ResponseServer; +import org.apache.hc.core5.http2.protocol.H2RequestConformance; import org.apache.hc.core5.http2.protocol.H2RequestConnControl; import org.apache.hc.core5.http2.protocol.H2RequestContent; import org.apache.hc.core5.http2.protocol.H2RequestTargetHost; import org.apache.hc.core5.http2.protocol.H2RequestValidateHost; +import org.apache.hc.core5.http2.protocol.H2ResponseConformance; import org.apache.hc.core5.http2.protocol.H2ResponseConnControl; import org.apache.hc.core5.http2.protocol.H2ResponseContent; import org.apache.hc.core5.util.TextUtils; @@ -55,6 +57,7 @@ public static HttpProcessorBuilder customServer(final String serverInfo) { return HttpProcessorBuilder.create() .addAll( ResponseConformance.INSTANCE, + H2ResponseConformance.INSTANCE, ResponseDate.INSTANCE, new ResponseServer(!TextUtils.isBlank(serverInfo) ? serverInfo : VersionInfo.getSoftwareInfo(SOFTWARE, "org.apache.hc.core5", H2Processors.class)), @@ -62,6 +65,7 @@ public static HttpProcessorBuilder customServer(final String serverInfo) { H2ResponseConnControl.INSTANCE) .addAll( H2RequestValidateHost.INSTANCE, + H2RequestConformance.INSTANCE, RequestConformance.INSTANCE); } @@ -76,6 +80,9 @@ public static HttpProcessor server() { public static HttpProcessorBuilder customClient(final String agentInfo) { return HttpProcessorBuilder.create() .addAll( + H2ResponseConformance.INSTANCE) + .addAll( + H2RequestConformance.INSTANCE, H2RequestTargetHost.INSTANCE, H2RequestContent.INSTANCE, H2RequestConnControl.INSTANCE, diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java index 431ee5b0b6..feca33a556 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java @@ -31,13 +31,10 @@ import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; -import java.nio.charset.CharacterCodingException; import java.util.Deque; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; @@ -60,12 +57,14 @@ import org.apache.hc.core5.http.impl.BasicEndpointDetails; import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; import org.apache.hc.core5.http.impl.CharCodingSupport; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncPushConsumer; import org.apache.hc.core5.http.nio.AsyncPushProducer; import org.apache.hc.core5.http.nio.HandlerFactory; -import org.apache.hc.core5.http.nio.command.ExecutableCommand; +import org.apache.hc.core5.http.nio.command.CommandSupport; +import org.apache.hc.core5.http.nio.command.RequestExecutionCommand; import org.apache.hc.core5.http.nio.command.ShutdownCommand; -import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http2.H2ConnectionException; import org.apache.hc.core5.http2.H2Error; @@ -83,6 +82,7 @@ import org.apache.hc.core5.http2.impl.BasicH2TransportMetrics; import org.apache.hc.core5.http2.nio.AsyncPingHandler; import org.apache.hc.core5.http2.nio.command.PingCommand; +import org.apache.hc.core5.http2.nio.command.PushResponseCommand; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.ProtocolIOSession; @@ -94,15 +94,13 @@ abstract class AbstractH2StreamMultiplexer implements Identifiable, HttpConnection { - private static final long LINGER_TIME = 1000; // 1 second private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024; // 10 MiB - enum ConnectionHandshake { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN} + enum ConnectionHandshake { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN } enum SettingsHandshake { READY, TRANSMITTED, ACKED } private final ProtocolIOSession ioSession; private final FrameFactory frameFactory; - private final StreamIdGenerator idGenerator; private final HttpProcessor httpProcessor; private final H2Config localConfig; private final BasicH2TransportMetrics inputMetrics; @@ -113,12 +111,11 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED } private final Deque outputQueue; private final HPackEncoder hPackEncoder; private final HPackDecoder hPackDecoder; - private final Map streamMap; + private final H2Streams streams; private final Queue pingHandlers; private final AtomicInteger connInputWindow; private final AtomicInteger connOutputWindow; private final AtomicInteger outputRequests; - private final AtomicInteger lastStreamId; private final H2StreamListener streamListener; private ConnectionHandshake connState = ConnectionHandshake.READY; @@ -133,7 +130,6 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED } private Continuation continuation; - private int processedRemoteStreamId; private EndpointDetails endpointDetails; private boolean goAwayReceived; @@ -147,8 +143,8 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED } final H2StreamListener streamListener) { this.ioSession = Args.notNull(ioSession, "IO session"); this.frameFactory = Args.notNull(frameFactory, "Frame factory"); - this.idGenerator = Args.notNull(idGenerator, "Stream id generator"); this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); + this.streams = new H2Streams(idGenerator); this.localConfig = h2Config != null ? h2Config : H2Config.DEFAULT; this.inputMetrics = new BasicH2TransportMetrics(); this.outputMetrics = new BasicH2TransportMetrics(); @@ -158,10 +154,8 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED } this.outputQueue = new ConcurrentLinkedDeque<>(); this.pingHandlers = new ConcurrentLinkedQueue<>(); this.outputRequests = new AtomicInteger(0); - this.lastStreamId = new AtomicInteger(0); - this.hPackEncoder = new HPackEncoder(CharCodingSupport.createEncoder(charCodingConfig)); - this.hPackDecoder = new HPackDecoder(CharCodingSupport.createDecoder(charCodingConfig)); - this.streamMap = new ConcurrentHashMap<>(); + this.hPackEncoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(charCodingConfig)); + this.hPackDecoder = new HPackDecoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createDecoder(charCodingConfig)); this.remoteConfig = H2Config.INIT; this.connInputWindow = new AtomicInteger(H2Config.INIT.getInitialWindowSize()); this.connOutputWindow = new AtomicInteger(H2Config.INIT.getInitialWindowSize()); @@ -169,8 +163,6 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED } this.initInputWinSize = H2Config.INIT.getInitialWindowSize(); this.initOutputWinSize = H2Config.INIT.getInitialWindowSize(); - this.hPackDecoder.setMaxTableSize(H2Config.INIT.getHeaderTableSize()); - this.hPackEncoder.setMaxTableSize(H2Config.INIT.getHeaderTableSize()); this.hPackDecoder.setMaxListSize(H2Config.INIT.getMaxHeaderListSize()); this.lowMark = H2Config.INIT.getInitialWindowSize() / 2; @@ -182,36 +174,47 @@ public String getId() { return ioSession.getId(); } + BasicHttpConnectionMetrics getConnMetrics() { + return connMetrics; + } + + HttpProcessor getHttpProcessor() { + return httpProcessor; + } + + void submitCommand(final Command command) { + ioSession.enqueue(command, Command.Priority.NORMAL); + } + + abstract void validateSetting(H2Param param, int value) throws H2ConnectionException; + + abstract H2Setting[] generateSettings(H2Config localConfig); + abstract void acceptHeaderFrame() throws H2ConnectionException; abstract void acceptPushRequest() throws H2ConnectionException; abstract void acceptPushFrame() throws H2ConnectionException; - abstract H2StreamHandler createRemotelyInitiatedStream( - H2StreamChannel channel, - HttpProcessor httpProcessor, - BasicHttpConnectionMetrics connMetrics, - HandlerFactory pushHandlerFactory) throws IOException; + abstract H2Stream incomingRequest(H2StreamChannel channel) throws IOException; + + abstract H2Stream incomingPushPromise(H2StreamChannel channel, + HandlerFactory pushHandlerFactory) throws IOException; - abstract H2StreamHandler createLocallyInitiatedStream( - ExecutableCommand command, - H2StreamChannel channel, - HttpProcessor httpProcessor, - BasicHttpConnectionMetrics connMetrics) throws IOException; + abstract H2Stream outgoingRequest(H2StreamChannel channel, + AsyncClientExchangeHandler exchangeHandler, + HandlerFactory pushHandlerFactory, + HttpContext context) throws IOException; + + abstract H2Stream outgoingPushPromise(H2StreamChannel channel, + AsyncPushProducer pushProducer) throws IOException; + + abstract boolean allowGracefulAbort(H2Stream stream); private int updateWindow(final AtomicInteger window, final int delta) throws ArithmeticException { for (;;) { final int current = window.get(); - long newValue = (long) current + delta; - - //TODO: work-around for what looks like a bug in Ngnix (1.11) - // Tolerate if the update window exceeded by one - if (newValue == 0x80000000L) { - newValue = Integer.MAX_VALUE; - } - //TODO: needs to be removed - + final long newValue = (long) current + delta; if (Math.abs(newValue) > 0x7fffffffL) { throw new ArithmeticException("Update causes flow control window to exceed " + Integer.MAX_VALUE); } @@ -221,6 +224,15 @@ private int updateWindow(final AtomicInteger window, final int delta) throws Ari } } + private int updateWindowMax(final AtomicInteger window) throws ArithmeticException { + for (;;) { + final int current = window.get(); + if (window.compareAndSet(current, Integer.MAX_VALUE)) { + return Integer.MAX_VALUE - current; + } + } + } + private int updateInputWindow( final int streamId, final AtomicInteger window, final int delta) throws ArithmeticException { final int newSize = updateWindow(window, delta); @@ -304,7 +316,7 @@ private void commitPushPromise( buf.append((byte)(promisedStreamId >> 24)); buf.append((byte)(promisedStreamId >> 16)); buf.append((byte)(promisedStreamId >> 8)); - buf.append((byte)(promisedStreamId)); + buf.append((byte) promisedStreamId); hPackEncoder.encodeHeaders(buf, headers, localConfig.isCompressionEnabled()); @@ -381,48 +393,25 @@ private void incrementInputCapacity( final int remainingCapacity = Integer.MAX_VALUE - streamWinSize; final int chunk = Math.min(inputCapacity, remainingCapacity); if (chunk != 0) { + updateInputWindow(streamId, inputWindow, chunk); final RawFrame windowUpdateFrame = frameFactory.createWindowUpdate(streamId, chunk); commitFrame(windowUpdateFrame); - updateInputWindow(streamId, inputWindow, chunk); } } } - private void requestSessionOutput() { + void requestSessionOutput() { outputRequests.incrementAndGet(); ioSession.setEvent(SelectionKey.OP_WRITE); } - private void updateLastStreamId(final int streamId) { - final int currentId = lastStreamId.get(); - if (streamId > currentId) { - lastStreamId.compareAndSet(currentId, streamId); - } - } - - private int generateStreamId() { - for (;;) { - final int currentId = lastStreamId.get(); - final int newStreamId = idGenerator.generate(currentId); - if (lastStreamId.compareAndSet(currentId, newStreamId)) { - return newStreamId; - } - } - } - public final void onConnect() throws HttpException, IOException { connState = ConnectionHandshake.ACTIVE; - final RawFrame settingsFrame = frameFactory.createSettings( - new H2Setting(H2Param.HEADER_TABLE_SIZE, localConfig.getHeaderTableSize()), - new H2Setting(H2Param.ENABLE_PUSH, localConfig.isPushEnabled() ? 1 : 0), - new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()), - new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()), - new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()), - new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize())); + final RawFrame settingsFrame = frameFactory.createSettings(generateSettings(localConfig)); commitFrame(settingsFrame); localSettingState = SettingsHandshake.TRANSMITTED; - maximizeConnWindow(connInputWindow.get()); + maximizeWindow(0, connInputWindow); if (streamListener != null) { final int initInputWindow = connInputWindow.get(); @@ -438,13 +427,22 @@ public final void onInput(final ByteBuffer src) throws HttpException, IOExceptio } else { for (;;) { final RawFrame frame = inputBuffer.read(src, ioSession); - if (frame == null) { + if (frame != null) { + if (streamListener != null) { + streamListener.onFrameInput(this, frame.getStreamId(), frame); + } + consumeFrame(frame); + } else { + if (inputBuffer.isEndOfStream()) { + if (connState == ConnectionHandshake.ACTIVE) { + final RawFrame goAway = frameFactory.createGoAway(streams.getLastRemoteId(), H2Error.NO_ERROR, "Unexpected end of stream"); + commitFrame(goAway); + } + connState = ConnectionHandshake.SHUTDOWN; + requestSessionOutput(); + } break; } - if (streamListener != null) { - streamListener.onFrameInput(this, frame.getStreamId(), frame); - } - consumeFrame(frame); } } } @@ -477,10 +475,9 @@ public final void onOutput() throws HttpException, IOException { } final int pendingOutputRequests = outputRequests.get(); boolean outputPending = false; - if (!streamMap.isEmpty() && connOutputWindow.get() > 0) { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); + if (!streams.isEmpty() && connOutputWindow.get() > 0) { + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); if (!stream.isLocalClosed() && stream.getOutputWindow().get() > 0 && stream.isOutputReady()) { @@ -503,18 +500,34 @@ public final void onOutput() throws HttpException, IOException { } if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0 && remoteSettingState == SettingsHandshake.ACKED) { - processPendingCommands(); + while (streams.size() < remoteConfig.getMaxConcurrentStreams()) { + final Command command = ioSession.poll(); + if (command == null) { + break; + } + if (command instanceof ShutdownCommand) { + executeShutdown((ShutdownCommand) command); + } else if (command instanceof PingCommand) { + executePing((PingCommand) command); + } else if (command instanceof RequestExecutionCommand) { + executeRequest((RequestExecutionCommand) command); + } else if (command instanceof PushResponseCommand) { + executePush((PushResponseCommand) command); + } + if (!outputQueue.isEmpty()) { + return; + } + } } if (connState.compareTo(ConnectionHandshake.GRACEFUL_SHUTDOWN) == 0) { int liveStreams = 0; - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); if (stream.isLocalClosed() && stream.isRemoteClosed()) { - stream.releaseResources(); + streams.release(stream); it.remove(); } else { - if (idGenerator.isSameSide(stream.getId()) || stream.getId() <= processedRemoteStreamId) { + if (streams.isSameSide(stream.getId()) || stream.getId() <= streams.getLastRemoteId()) { liveStreams++; } } @@ -523,13 +536,24 @@ public final void onOutput() throws HttpException, IOException { connState = ConnectionHandshake.SHUTDOWN; } } - if (connState.compareTo(ConnectionHandshake.SHUTDOWN) >= 0) { - if (!streamMap.isEmpty()) { - for (final H2Stream stream : streamMap.values()) { - stream.releaseResources(); + if (connState.compareTo(ConnectionHandshake.GRACEFUL_SHUTDOWN) >= 0) { + for (;;) { + final Command command = ioSession.poll(); + if (command == null) { + break; + } + if (command instanceof ShutdownCommand) { + final ShutdownCommand shutdownCommand = (ShutdownCommand) command; + if (shutdownCommand.getType() == CloseMode.IMMEDIATE) { + connState = ConnectionHandshake.SHUTDOWN; + } + } else { + command.cancel(); } - streamMap.clear(); } + } + if (connState.compareTo(ConnectionHandshake.SHUTDOWN) >= 0) { + streams.shutdownAndReleaseAll(); ioSession.getLock().lock(); try { if (outputBuffer.isEmpty() && outputQueue.isEmpty()) { @@ -546,19 +570,18 @@ public final void onTimeout(final Timeout timeout) throws HttpException, IOExcep final RawFrame goAway; if (localSettingState != SettingsHandshake.ACKED) { - goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.SETTINGS_TIMEOUT, - "Setting timeout (" + timeout + ")"); + goAway = frameFactory.createGoAway(streams.getLastRemoteId(), H2Error.SETTINGS_TIMEOUT, + "Setting timeout (" + timeout + ")"); } else { - goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.NO_ERROR, - "Timeout due to inactivity (" + timeout + ")"); + goAway = frameFactory.createGoAway(streams.getLastRemoteId(), H2Error.NO_ERROR, + "Timeout due to inactivity (" + timeout + ")"); } commitFrame(goAway); - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); - stream.reset(new H2StreamResetException(H2Error.NO_ERROR, "Timeout due to inactivity (" + timeout + ")")); + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); + stream.fail(new H2StreamResetException(H2Error.NO_ERROR, "Timeout due to inactivity (" + timeout + ")")); } - streamMap.clear(); + streams.shutdownAndReleaseAll(); } public final void onDisconnect() { @@ -570,83 +593,68 @@ public final void onDisconnect() { break; } } - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); - stream.cancel(); - } - for (;;) { - final Command command = ioSession.poll(); - if (command != null) { - if (command instanceof ExecutableCommand) { - ((ExecutableCommand) command).failed(new ConnectionClosedException()); - } else { - command.cancel(); - } - } else { - break; + streams.shutdownAndReleaseAll(); + CommandSupport.cancelCommands(ioSession); + } + + private void executeShutdown(final ShutdownCommand shutdownCommand) throws IOException { + if (shutdownCommand.getType() == CloseMode.IMMEDIATE) { + streams.shutdownAndReleaseAll(); + connState = ConnectionHandshake.SHUTDOWN; + } else { + if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0) { + final RawFrame goAway = frameFactory.createGoAway(streams.getLastRemoteId(), H2Error.NO_ERROR, "Graceful shutdown"); + commitFrame(goAway); + connState = streams.isEmpty() ? ConnectionHandshake.SHUTDOWN : ConnectionHandshake.GRACEFUL_SHUTDOWN; } } } - private void processPendingCommands() throws IOException, HttpException { - while (streamMap.size() < remoteConfig.getMaxConcurrentStreams()) { - final Command command = ioSession.poll(); - if (command == null) { - break; - } - if (command instanceof ShutdownCommand) { - final ShutdownCommand shutdownCommand = (ShutdownCommand) command; - if (shutdownCommand.getType() == CloseMode.IMMEDIATE) { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); - stream.cancel(); - } - streamMap.clear(); - connState = ConnectionHandshake.SHUTDOWN; - } else { - if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0) { - final RawFrame goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.NO_ERROR, "Graceful shutdown"); - commitFrame(goAway); - connState = streamMap.isEmpty() ? ConnectionHandshake.SHUTDOWN : ConnectionHandshake.GRACEFUL_SHUTDOWN; - } - } - break; - } else if (command instanceof PingCommand) { - final PingCommand pingCommand = (PingCommand) command; - final AsyncPingHandler handler = pingCommand.getHandler(); - pingHandlers.add(handler); - final RawFrame ping = frameFactory.createPing(handler.getData()); - commitFrame(ping); - } else if (command instanceof ExecutableCommand) { - final int streamId = generateStreamId(); - final H2StreamChannelImpl channel = new H2StreamChannelImpl( - streamId, true, initInputWinSize, initOutputWinSize); - final ExecutableCommand executableCommand = (ExecutableCommand) command; - final H2StreamHandler streamHandler = createLocallyInitiatedStream( - executableCommand, channel, httpProcessor, connMetrics); - - final H2Stream stream = new H2Stream(channel, streamHandler, false); - streamMap.put(streamId, stream); - - if (streamListener != null) { - final int initInputWindow = stream.getInputWindow().get(); - streamListener.onInputFlowControl(this, streamId, initInputWindow, initInputWindow); - final int initOutputWindow = stream.getOutputWindow().get(); - streamListener.onOutputFlowControl(this, streamId, initOutputWindow, initOutputWindow); - } + private void executePing(final PingCommand pingCommand) throws IOException { + final AsyncPingHandler handler = pingCommand.getHandler(); + pingHandlers.add(handler); + final RawFrame ping = frameFactory.createPing(handler.getData()); + commitFrame(ping); + } + + private void executeRequest(final RequestExecutionCommand requestExecutionCommand) throws IOException, HttpException { + final int streamId = streams.generateStreamId(); + final H2StreamChannel channel = createChannel(streamId); + final H2Stream stream = outgoingRequest(channel, + requestExecutionCommand.getExchangeHandler(), + requestExecutionCommand.getPushHandlerFactory(), + requestExecutionCommand.getContext()); + streams.addLocallyInitiated(stream); + + if (streamListener != null) { + final int initInputWindow = stream.getInputWindow().get(); + streamListener.onInputFlowControl(this, streamId, initInputWindow, initInputWindow); + final int initOutputWindow = stream.getOutputWindow().get(); + streamListener.onOutputFlowControl(this, streamId, initOutputWindow, initOutputWindow); + } + if (stream.isOutputReady()) { + stream.produceOutput(); + } + final CancellableDependency cancellableDependency = requestExecutionCommand.getCancellableDependency(); + if (cancellableDependency != null) { + cancellableDependency.setDependency(stream::abort); + } + } + + private void executePush(final PushResponseCommand pushResponseCommand) throws IOException, HttpException { + if (pushResponseCommand.isCancelled()) { + return; + } + final H2Stream stream = streams.lookupSeen(pushResponseCommand.getStreamId()); + if (stream != null && stream.isReserved()) { + if (!stream.isRemoteClosed()) { + stream.activate(); if (stream.isOutputReady()) { stream.produceOutput(); } - final CancellableDependency cancellableDependency = executableCommand.getCancellableDependency(); - if (cancellableDependency != null) { - cancellableDependency.setDependency(stream::abort); - } - if (!outputQueue.isEmpty()) { - return; - } + } else { + stream.abort(); } } } @@ -661,35 +669,21 @@ public final void onException(final Exception cause) { break; } } - for (;;) { - final Command command = ioSession.poll(); - if (command != null) { - if (command instanceof ExecutableCommand) { - ((ExecutableCommand) command).failed(new ConnectionClosedException()); - } else { - command.cancel(); - } - } else { - break; - } - } - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); - stream.reset(cause); - } - streamMap.clear(); + + CommandSupport.cancelCommands(ioSession); + streams.shutdownAndReleaseAll(); + if (!(cause instanceof ConnectionClosedException)) { if (connState.compareTo(ConnectionHandshake.GRACEFUL_SHUTDOWN) <= 0) { final H2Error errorCode; if (cause instanceof H2ConnectionException) { errorCode = H2Error.getByCode(((H2ConnectionException) cause).getCode()); - } else if (cause instanceof ProtocolException){ + } else if (cause instanceof ProtocolException) { errorCode = H2Error.PROTOCOL_ERROR; } else { errorCode = H2Error.INTERNAL_ERROR; } - final RawFrame goAway = frameFactory.createGoAway(processedRemoteStreamId, errorCode, cause.getMessage()); + final RawFrame goAway = frameFactory.createGoAway(streams.getLastRemoteId(), errorCode, cause.getMessage()); commitFrame(goAway); } } @@ -710,21 +704,6 @@ public final void onException(final Exception cause) { } } - private H2Stream getValidStream(final int streamId) throws H2ConnectionException { - if (streamId == 0) { - throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); - } - final H2Stream stream = streamMap.get(streamId); - if (stream == null) { - if (streamId <= lastStreamId.get()) { - throw new H2ConnectionException(H2Error.STREAM_CLOSED, "Stream closed"); - } else { - throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected stream id: " + streamId); - } - } - return stream; - } - private void consumeFrame(final RawFrame frame) throws HttpException, IOException { final FrameType frameType = FrameType.valueOf(frame.getType()); final int streamId = frame.getStreamId(); @@ -733,7 +712,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio } switch (frameType) { case DATA: { - final H2Stream stream = getValidStream(streamId); + if (streamId == 0) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); + } + final H2Stream stream = streams.lookupValid(streamId); try { consumeDataFrame(frame, stream); } catch (final H2StreamResetException ex) { @@ -743,8 +725,7 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio } if (stream.isTerminated()) { - streamMap.remove(streamId); - stream.releaseResources(); + streams.release(stream); requestSessionOutput(); } } @@ -753,39 +734,31 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio if (streamId == 0) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); } - H2Stream stream = streamMap.get(streamId); + H2Stream stream = streams.lookupValidOrNull(streamId); if (stream == null) { acceptHeaderFrame(); - - if (idGenerator.isSameSide(streamId)) { + if (streams.isSameSide(streamId)) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); } if (goAwayReceived ) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "GOAWAY received"); } - updateLastStreamId(streamId); - - final H2StreamChannelImpl channel = new H2StreamChannelImpl( - streamId, false, initInputWinSize, initOutputWinSize); - final H2StreamHandler streamHandler; + final H2StreamChannel channel = createChannel(streamId); if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0) { - streamHandler = createRemotelyInitiatedStream(channel, httpProcessor, connMetrics, null); + stream = incomingRequest(channel); } else { - streamHandler = NoopH2StreamHandler.INSTANCE; - channel.setLocalEndStream(); + stream = new H2Stream(channel, NoopH2StreamHandler.INSTANCE); + channel.localReset(H2Error.REFUSED_STREAM); } - - stream = new H2Stream(channel, streamHandler, true); - if (stream.isOutputReady()) { - stream.produceOutput(); - } - streamMap.put(streamId, stream); + streams.addRemotelyInitiated(stream); + } else if (stream.isLocalClosed() && stream.isRemoteClosed()) { + throw new H2ConnectionException(H2Error.STREAM_CLOSED, "Stream closed"); + } else if (stream.isReserved()) { + stream.activate(); } - try { consumeHeaderFrame(frame, stream); - if (stream.isOutputReady()) { stream.produceOutput(); } @@ -798,13 +771,15 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio } if (stream.isTerminated()) { - streamMap.remove(streamId); - stream.releaseResources(); + streams.release(stream); requestSessionOutput(); } } break; case CONTINUATION: { + if (streamId == 0) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); + } if (continuation == null) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected CONTINUATION frame"); } @@ -812,9 +787,8 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected CONTINUATION stream id: " + streamId); } - final H2Stream stream = getValidStream(streamId); + final H2Stream stream = streams.lookupValid(streamId); try { - consumeContinuationFrame(frame, stream); } catch (final H2StreamResetException ex) { stream.localReset(ex); @@ -823,8 +797,7 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio } if (stream.isTerminated()) { - streamMap.remove(streamId); - stream.releaseResources(); + streams.release(stream); requestSessionOutput(); } } @@ -845,7 +818,7 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio throw new H2ConnectionException(H2Error.FLOW_CONTROL_ERROR, ex.getMessage()); } } else { - final H2Stream stream = streamMap.get(streamId); + final H2Stream stream = streams.lookup(streamId); if (stream != null) { try { updateOutputWindow(streamId, stream.getOutputWindow(), delta); @@ -861,21 +834,21 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio if (streamId == 0) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); } - final H2Stream stream = streamMap.get(streamId); - if (stream == null) { - if (streamId > lastStreamId.get()) { - throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected stream id: " + streamId); - } - } else { + final H2Stream stream = streams.lookupValidOrNull(streamId); + if (stream != null) { final ByteBuffer payload = frame.getPayload(); if (payload == null || payload.remaining() != 4) { throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid RST_STREAM frame payload"); } final int errorCode = payload.getInt(); - stream.reset(new H2StreamResetException(errorCode, "Stream reset (" + errorCode + ")")); - streamMap.remove(streamId); - stream.releaseResources(); - requestSessionOutput(); + if (errorCode == H2Error.NO_ERROR.getCode() && allowGracefulAbort(stream)) { + stream.abortGracefully(); + requestSessionOutput(); + } else { + stream.fail(new H2StreamResetException(errorCode, "Stream reset (" + errorCode + ")")); + streams.release(stream); + requestSessionOutput(); + } } } break; @@ -932,6 +905,9 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio break; case PUSH_PROMISE: { acceptPushFrame(); + if (streamId == 0) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId); + } if (goAwayReceived ) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "GOAWAY received"); @@ -941,7 +917,7 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Push is disabled"); } - final H2Stream stream = getValidStream(streamId); + final H2Stream stream = streams.lookupValid(streamId); if (stream.isRemoteClosed()) { stream.localReset(new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream closed")); break; @@ -952,29 +928,22 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid PUSH_PROMISE payload"); } final int promisedStreamId = payload.getInt(); - if (promisedStreamId == 0 || idGenerator.isSameSide(promisedStreamId)) { + if (promisedStreamId == 0 || streams.isSameSide(promisedStreamId)) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal promised stream id: " + promisedStreamId); } - if (streamMap.get(promisedStreamId) != null) { - throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected promised stream id: " + promisedStreamId); + if (streams.lookupValidOrNull(promisedStreamId) != null) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Stream already open: " + promisedStreamId); } - updateLastStreamId(promisedStreamId); - - final H2StreamChannelImpl channel = new H2StreamChannelImpl( - promisedStreamId, false, initInputWinSize, initOutputWinSize); - final H2StreamHandler streamHandler; + final H2StreamChannel channel = createChannel(promisedStreamId); + final H2Stream promisedStream; if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0) { - streamHandler = createRemotelyInitiatedStream(channel, httpProcessor, connMetrics, - stream.getPushHandlerFactory()); + promisedStream = incomingPushPromise(channel, stream.getPushHandlerFactory()); } else { - streamHandler = NoopH2StreamHandler.INSTANCE; - channel.setLocalEndStream(); + promisedStream = new H2Stream(channel, NoopH2StreamHandler.INSTANCE); + channel.localReset(H2Error.REFUSED_STREAM); } - - final H2Stream promisedStream = new H2Stream(channel, streamHandler, true); - streamMap.put(promisedStreamId, promisedStream); - + streams.addRemotelyInitiated(promisedStream); try { consumePushPromiseFrame(frame, payload, promisedStream); } catch (final H2StreamResetException ex) { @@ -997,24 +966,22 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio goAwayReceived = true; if (errorCode == H2Error.NO_ERROR.getCode()) { if (connState.compareTo(ConnectionHandshake.ACTIVE) <= 0) { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final int activeStreamId = entry.getKey(); - if (!idGenerator.isSameSide(activeStreamId) && activeStreamId > processedLocalStreamId) { - final H2Stream stream = entry.getValue(); - stream.cancel(); + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); + final int activeStreamId = stream.getId(); + if (!streams.isSameSide(activeStreamId) && activeStreamId > processedLocalStreamId) { + stream.fail(new RequestNotExecutedException()); it.remove(); } } } - connState = streamMap.isEmpty() ? ConnectionHandshake.SHUTDOWN : ConnectionHandshake.GRACEFUL_SHUTDOWN; + connState = streams.isEmpty() ? ConnectionHandshake.SHUTDOWN : ConnectionHandshake.GRACEFUL_SHUTDOWN; } else { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); - stream.reset(new H2StreamResetException(errorCode, "Connection terminated by the peer (" + errorCode + ")")); + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); + stream.fail(new H2StreamResetException(errorCode, "Connection terminated by the peer (" + errorCode + ")")); } - streamMap.clear(); + streams.shutdownAndReleaseAll(); connState = ConnectionHandshake.SHUTDOWN; } } @@ -1024,6 +991,9 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio } private void consumeDataFrame(final RawFrame frame, final H2Stream stream) throws HttpException, IOException { + if (stream.isRemoteClosed()) { + throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed"); + } final int streamId = stream.getId(); final ByteBuffer payload = frame.getPayloadContent(); if (payload != null) { @@ -1034,44 +1004,33 @@ private void consumeDataFrame(final RawFrame frame, final H2Stream stream) throw } final int connWinSize = updateInputWindow(0, connInputWindow, -frameLength); if (connWinSize < CONNECTION_WINDOW_LOW_MARK) { - maximizeConnWindow(connWinSize); + maximizeWindow(0, connInputWindow); } } - if (stream.isRemoteClosed()) { - throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed"); - } - if (frame.isFlagSet(FrameFlag.END_STREAM)) { - stream.setRemoteEndStream(); - } - if (stream.isLocalReset()) { - return; - } - stream.consumeData(payload); + stream.consumeData(payload, frame.isFlagSet(FrameFlag.END_STREAM)); } - private void maximizeConnWindow(final int connWinSize) throws IOException { - final int delta = Integer.MAX_VALUE - connWinSize; + private void maximizeWindow(final int streamId, final AtomicInteger window) throws IOException { + final int delta = updateWindowMax(window); if (delta > 0) { - final RawFrame windowUpdateFrame = frameFactory.createWindowUpdate(0, delta); + final RawFrame windowUpdateFrame = frameFactory.createWindowUpdate(streamId, delta); commitFrame(windowUpdateFrame); - updateInputWindow(0, connInputWindow, delta); } } private void consumePushPromiseFrame(final RawFrame frame, final ByteBuffer payload, final H2Stream promisedStream) throws HttpException, IOException { final int promisedStreamId = promisedStream.getId(); + if (!frame.isFlagSet(FrameFlag.END_HEADERS)) { - continuation = new Continuation(promisedStreamId, frame.getType(), true); + continuation = new Continuation(promisedStreamId, frame.getType(), true, + localConfig.getMaxContinuations()); } if (continuation == null) { final List
headers = hPackDecoder.decodeHeaders(payload); - if (promisedStreamId > processedRemoteStreamId) { - processedRemoteStreamId = promisedStreamId; - } if (streamListener != null) { streamListener.onHeaderInput(this, promisedStreamId, headers); } - promisedStream.consumePromise(headers); + promisedStream.consumePromise(headers, false); } else { continuation.copyPayload(payload); } @@ -1082,9 +1041,13 @@ List
decodeHeaders(final ByteBuffer payload) throws HttpException { } private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) throws HttpException, IOException { + if (stream.isRemoteClosed()) { + throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed"); + } final int streamId = stream.getId(); if (!frame.isFlagSet(FrameFlag.END_HEADERS)) { - continuation = new Continuation(streamId, frame.getType(), frame.isFlagSet(FrameFlag.END_STREAM)); + continuation = new Continuation(streamId, frame.getType(), frame.isFlagSet(FrameFlag.END_STREAM), + localConfig.getMaxContinuations()); } final ByteBuffer payload = frame.getPayloadContent(); if (frame.isFlagSet(FrameFlag.PRIORITY)) { @@ -1094,52 +1057,31 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr } if (continuation == null) { final List
headers = decodeHeaders(payload); - if (stream.isRemoteInitiated() && streamId > processedRemoteStreamId) { - processedRemoteStreamId = streamId; - } if (streamListener != null) { streamListener.onHeaderInput(this, streamId, headers); } - if (stream.isRemoteClosed()) { - throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed"); - } - if (stream.isLocalReset()) { - return; - } - if (frame.isFlagSet(FrameFlag.END_STREAM)) { - stream.setRemoteEndStream(); - } - stream.consumeHeader(headers); + stream.consumeHeader(headers, frame.isFlagSet(FrameFlag.END_STREAM)); } else { continuation.copyPayload(payload); } } private void consumeContinuationFrame(final RawFrame frame, final H2Stream stream) throws HttpException, IOException { + if (stream.isRemoteClosed()) { + throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed"); + } final int streamId = frame.getStreamId(); final ByteBuffer payload = frame.getPayload(); continuation.copyPayload(payload); if (frame.isFlagSet(FrameFlag.END_HEADERS)) { final List
headers = decodeHeaders(continuation.getContent()); - if (stream.isRemoteInitiated() && streamId > processedRemoteStreamId) { - processedRemoteStreamId = streamId; - } if (streamListener != null) { streamListener.onHeaderInput(this, streamId, headers); } - if (stream.isRemoteClosed()) { - throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed"); - } - if (stream.isLocalReset()) { - return; - } - if (continuation.endStream) { - stream.setRemoteEndStream(); - } if (continuation.type == FrameType.PUSH_PROMISE.getValue()) { - stream.consumePromise(headers); + stream.consumePromise(headers, continuation.endStream); } else { - stream.consumeHeader(headers); + stream.consumeHeader(headers, continuation.endStream); } continuation = null; } @@ -1152,6 +1094,7 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException { final int value = payload.getInt(); final H2Param param = H2Param.valueOf(code); if (param != null) { + validateSetting(param, value); switch (param) { case HEADER_TABLE_SIZE: try { @@ -1198,15 +1141,14 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException { } private void produceOutput() throws HttpException, IOException { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); if (!stream.isLocalClosed() && stream.getOutputWindow().get() > 0) { stream.produceOutput(); } if (stream.isTerminated()) { it.remove(); - stream.releaseResources(); + streams.release(stream); requestSessionOutput(); } if (!outputQueue.isEmpty()) { @@ -1232,10 +1174,9 @@ private void applyRemoteSettings(final H2Config config) throws H2ConnectionExcep } if (delta != 0) { - if (!streamMap.isEmpty()) { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); + if (!streams.isEmpty()) { + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); try { updateOutputWindow(stream.getId(), stream.getOutputWindow(), delta); } catch (final ArithmeticException ex) { @@ -1253,10 +1194,9 @@ private void applyLocalSettings() throws H2ConnectionException { final int delta = localConfig.getInitialWindowSize() - initInputWinSize; initInputWinSize = localConfig.getInitialWindowSize(); - if (delta != 0 && !streamMap.isEmpty()) { - for (final Iterator> it = streamMap.entrySet().iterator(); it.hasNext(); ) { - final Map.Entry entry = it.next(); - final H2Stream stream = entry.getValue(); + if (delta != 0 && !streams.isEmpty()) { + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); try { updateInputWindow(stream.getId(), stream.getInputWindow(), delta); } catch (final ArithmeticException ex) { @@ -1330,8 +1270,9 @@ void appendState(final StringBuilder buf) { .append(", connInputWindow=").append(connInputWindow) .append(", connOutputWindow=").append(connOutputWindow) .append(", outputQueue=").append(outputQueue.size()) - .append(", streamMap=").append(streamMap.size()) - .append(", processedRemoteStreamId=").append(processedRemoteStreamId); + .append(", streams.size=").append(streams.size()) + .append(", streams.lastLocal=").append(streams.getLastLocalId()) + .append(", streams.lastRemote=").append(streams.getLastRemoteId()); } private static class Continuation { @@ -1340,20 +1281,33 @@ private static class Continuation { final int type; final boolean endStream; final ByteArrayBuffer headerBuffer; + final int maxContinuation; + final boolean enforceMacContinuations; - private Continuation(final int streamId, final int type, final boolean endStream) { + private int count; + + private Continuation(final int streamId, final int type, final boolean endStream, final int maxContinuation) { this.streamId = streamId; this.type = type; this.endStream = endStream; + this.maxContinuation = maxContinuation; + this.enforceMacContinuations = maxContinuation < Integer.MAX_VALUE; this.headerBuffer = new ByteArrayBuffer(1024); } - void copyPayload(final ByteBuffer payload) { + void copyPayload(final ByteBuffer payload) throws H2ConnectionException { if (payload == null) { return; } - headerBuffer.ensureCapacity(payload.remaining()); - payload.get(headerBuffer.array(), headerBuffer.length(), payload.remaining()); + if (enforceMacContinuations && count > maxContinuation) { + throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM, "Excessive number of continuation frames"); + } + count++; + final int originalLength = headerBuffer.length(); + final int toCopy = payload.remaining(); + headerBuffer.ensureCapacity(toCopy); + payload.get(headerBuffer.array(), originalLength, toCopy); + headerBuffer.setLength(originalLength + toCopy); } ByteBuffer getContent() { @@ -1362,37 +1316,50 @@ ByteBuffer getContent() { } - private class H2StreamChannelImpl implements H2StreamChannel { + H2StreamChannel createChannel(final int streamId) { + return new H2StreamChannelImpl(streamId, initInputWinSize, initOutputWinSize); + } + + void addStream(final H2Stream stream) throws H2ConnectionException { + streams.addLocallyInitiated(stream); + } + + class H2StreamChannelImpl implements H2StreamChannel { private final int id; private final AtomicInteger inputWindow; private final AtomicInteger outputWindow; - private volatile boolean idle; - private volatile boolean remoteEndStream; - private volatile boolean localEndStream; + private volatile boolean localClosed; + private volatile long localResetTime; - private volatile long deadline; - - H2StreamChannelImpl(final int id, final boolean idle, final int initialInputWindowSize, final int initialOutputWindowSize) { + H2StreamChannelImpl(final int id, final int initialInputWindowSize, final int initialOutputWindowSize) { this.id = id; - this.idle = idle; this.inputWindow = new AtomicInteger(initialInputWindowSize); this.outputWindow = new AtomicInteger(initialOutputWindowSize); } - int getId() { + @Override + public int getId() { return id; } - AtomicInteger getOutputWindow() { + @Override + public AtomicInteger getOutputWindow() { return outputWindow; } - AtomicInteger getInputWindow() { + @Override + public AtomicInteger getInputWindow() { return inputWindow; } + void ensureNotClosed() throws H2ConnectionException { + if (localClosed) { + throw new H2ConnectionException(H2Error.INTERNAL_ERROR, "Stream already closed locally"); + } + } + @Override public void submit(final List
headers, final boolean endStream) throws IOException { ioSession.getLock().lock(); @@ -1400,13 +1367,10 @@ public void submit(final List
headers, final boolean endStream) throws I if (headers == null || headers.isEmpty()) { throw new H2ConnectionException(H2Error.INTERNAL_ERROR, "Message headers are missing"); } - if (localEndStream) { - return; - } - idle = false; + ensureNotClosed(); commitHeaders(id, headers, endStream); if (endStream) { - localEndStream = true; + localClosed = true; } } finally { ioSession.getLock().unlock(); @@ -1416,28 +1380,16 @@ public void submit(final List
headers, final boolean endStream) throws I @Override public void push(final List
headers, final AsyncPushProducer pushProducer) throws HttpException, IOException { acceptPushRequest(); - final int promisedStreamId = generateStreamId(); - final H2StreamChannelImpl channel = new H2StreamChannelImpl( - promisedStreamId, - true, - localConfig.getInitialWindowSize(), - remoteConfig.getInitialWindowSize()); - final HttpCoreContext context = HttpCoreContext.create(); - context.setSSLSession(getSSLSession()); - context.setEndpointDetails(getEndpointDetails()); - final H2StreamHandler streamHandler = new ServerPushH2StreamHandler( - channel, httpProcessor, connMetrics, pushProducer, context); - final H2Stream stream = new H2Stream(channel, streamHandler, false); - streamMap.put(promisedStreamId, stream); - ioSession.getLock().lock(); try { - if (localEndStream) { - stream.releaseResources(); - return; - } + ensureNotClosed(); + final int promisedStreamId = streams.generateStreamId(); + final H2StreamChannel channel = createChannel(promisedStreamId); + final H2Stream stream = outgoingPushPromise(channel, pushProducer); + streams.addLocallyInitiated(stream); + commitPushPromise(id, promisedStreamId, headers); - idle = false; + submitCommand(new PushResponseCommand(promisedStreamId)); } finally { ioSession.getLock().unlock(); } @@ -1445,9 +1397,6 @@ public void push(final List
headers, final AsyncPushProducer pushProduce @Override public void update(final int increment) throws IOException { - if (remoteEndStream) { - return; - } incrementInputCapacity(0, connInputWindow, increment); incrementInputCapacity(id, inputWindow, increment); } @@ -1456,9 +1405,7 @@ public void update(final int increment) throws IOException { public int write(final ByteBuffer payload) throws IOException { ioSession.getLock().lock(); try { - if (localEndStream) { - return 0; - } + ensureNotClosed(); return streamData(id, outputWindow, payload); } finally { ioSession.getLock().unlock(); @@ -1469,10 +1416,8 @@ public int write(final ByteBuffer payload) throws IOException { public void endStream(final List trailers) throws IOException { ioSession.getLock().lock(); try { - if (localEndStream) { - return; - } - localEndStream = true; + ensureNotClosed(); + localClosed = true; if (trailers != null && !trailers.isEmpty()) { commitHeaders(id, trailers, true); } else { @@ -1494,52 +1439,38 @@ public void requestOutput() { requestSessionOutput(); } - boolean isRemoteClosed() { - return remoteEndStream; - } - - void setRemoteEndStream() { - remoteEndStream = true; - } - - boolean isLocalClosed() { - return localEndStream; - } - - void setLocalEndStream() { - localEndStream = true; - } - - boolean isLocalReset() { - return deadline > 0; + @Override + public boolean isLocalClosed() { + return localClosed; } - boolean isResetDeadline() { - final long l = deadline; - return l > 0 && l < System.currentTimeMillis(); + @Override + public void markLocalClosed() { + localClosed = true; } - boolean localReset(final int code) throws IOException { + @Override + public boolean localReset(final int code) throws IOException { ioSession.getLock().lock(); try { - if (localEndStream) { + if (isLocalReset()) { return false; } - localEndStream = true; - deadline = System.currentTimeMillis() + LINGER_TIME; - if (!idle) { - final RawFrame resetStream = frameFactory.createResetStream(id, code); - commitFrameInternal(resetStream); - return true; - } - return false; + ensureNotClosed(); + localClosed = true; + localResetTime = System.currentTimeMillis(); + + final RawFrame resetStream = frameFactory.createResetStream(id, code); + commitFrameInternal(resetStream); + return true; } finally { ioSession.getLock().unlock(); } } - boolean localReset(final H2Error error) throws IOException { - return localReset(error!= null ? error.getCode() : H2Error.INTERNAL_ERROR.getCode()); + @Override + public long getLocalResetTime() { + return localResetTime; } @Override @@ -1551,173 +1482,16 @@ public boolean cancel() { } } - void appendState(final StringBuilder buf) { - buf.append("id=").append(id) - .append(", connState=").append(connState) - .append(", inputWindow=").append(inputWindow) - .append(", outputWindow=").append(outputWindow) - .append(", localEndStream=").append(localEndStream) - .append(", idle=").append(idle); - } - - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); - buf.append("["); - appendState(buf); - buf.append("]"); - return buf.toString(); - } - - } - - static class H2Stream { - - private final H2StreamChannelImpl channel; - private final H2StreamHandler handler; - private final boolean remoteInitiated; - - private H2Stream( - final H2StreamChannelImpl channel, - final H2StreamHandler handler, - final boolean remoteInitiated) { - this.channel = channel; - this.handler = handler; - this.remoteInitiated = remoteInitiated; - } - - int getId() { - return channel.getId(); - } - - boolean isRemoteInitiated() { - return remoteInitiated; - } - - AtomicInteger getOutputWindow() { - return channel.getOutputWindow(); - } - - AtomicInteger getInputWindow() { - return channel.getInputWindow(); - } - - boolean isTerminated() { - return channel.isLocalClosed() && (channel.isRemoteClosed() || channel.isResetDeadline()); - } - - boolean isRemoteClosed() { - return channel.isRemoteClosed(); - } - - boolean isLocalClosed() { - return channel.isLocalClosed(); - } - - boolean isLocalReset() { - return channel.isLocalReset(); - } - - void setRemoteEndStream() { - channel.setRemoteEndStream(); - } - - void consumePromise(final List
headers) throws HttpException, IOException { - try { - handler.consumePromise(headers); - channel.setLocalEndStream(); - } catch (final ProtocolException ex) { - localReset(ex, H2Error.PROTOCOL_ERROR); - } - } - - void consumeHeader(final List
headers) throws HttpException, IOException { - try { - handler.consumeHeader(headers, channel.isRemoteClosed()); - } catch (final ProtocolException ex) { - localReset(ex, H2Error.PROTOCOL_ERROR); - } - } - - void consumeData(final ByteBuffer src) throws HttpException, IOException { - try { - handler.consumeData(src, channel.isRemoteClosed()); - } catch (final CharacterCodingException ex) { - localReset(ex, H2Error.INTERNAL_ERROR); - } catch (final ProtocolException ex) { - localReset(ex, H2Error.PROTOCOL_ERROR); - } - } - - boolean isOutputReady() { - return handler.isOutputReady(); - } - - void produceOutput() throws HttpException, IOException { - try { - handler.produceOutput(); - } catch (final ProtocolException ex) { - localReset(ex, H2Error.PROTOCOL_ERROR); - } - } - - void produceInputCapacityUpdate() throws IOException { - handler.updateInputCapacity(); - } - - void reset(final Exception cause) { - channel.setRemoteEndStream(); - channel.setLocalEndStream(); - handler.failed(cause); - } - - void localReset(final Exception cause, final int code) throws IOException { - channel.localReset(code); - handler.failed(cause); - } - - void localReset(final Exception cause, final H2Error error) throws IOException { - localReset(cause, error != null ? error.getCode() : H2Error.INTERNAL_ERROR.getCode()); - } - - void localReset(final H2StreamResetException ex) throws IOException { - localReset(ex, ex.getCode()); - } - - void handle(final HttpException ex) throws IOException, HttpException { - handler.handle(ex, channel.isRemoteClosed()); - } - - HandlerFactory getPushHandlerFactory() { - return handler.getPushHandlerFactory(); - } - - void cancel() { - reset(new RequestNotExecutedException()); - } - - boolean abort() { - final boolean cancelled = channel.cancel(); - handler.failed(new RequestNotExecutedException()); - return cancelled; - } - - void releaseResources() { - handler.releaseResources(); - } - - void appendState(final StringBuilder buf) { - buf.append("channel=["); - channel.appendState(buf); - buf.append("]"); - } - @Override public String toString() { final StringBuilder buf = new StringBuilder(); - buf.append("["); - appendState(buf); - buf.append("]"); + buf.append("[") + .append("id=").append(id) + .append(", connState=").append(connState) + .append(", inputWindow=").append(inputWindow) + .append(", outputWindow=").append(outputWindow) + .append(", localClosed=").append(localClosed) + .append("]"); return buf.toString(); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2PrefaceHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2PrefaceHandler.java index 41d3800ca6..02ae9628c7 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2PrefaceHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2PrefaceHandler.java @@ -34,6 +34,7 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.impl.nio.BufferedData; import org.apache.hc.core5.http2.ssl.ApplicationProtocol; import org.apache.hc.core5.reactor.IOSession; @@ -68,8 +69,9 @@ public class ClientH2PrefaceHandler extends PrefaceHandlerBase { public ClientH2PrefaceHandler( final ProtocolIOSession ioSession, final ClientH2StreamMultiplexerFactory http2StreamHandlerFactory, - final boolean strictALPNHandshake) { - this(ioSession, http2StreamHandlerFactory, strictALPNHandshake, null); + final boolean strictALPNHandshake, + final Callback exceptionCallback) { + this(ioSession, http2StreamHandlerFactory, strictALPNHandshake, null, exceptionCallback); } /** @@ -79,8 +81,9 @@ public ClientH2PrefaceHandler( final ProtocolIOSession ioSession, final ClientH2StreamMultiplexerFactory http2StreamHandlerFactory, final boolean strictALPNHandshake, - final FutureCallback resultCallback) { - super(ioSession, resultCallback); + final FutureCallback resultCallback, + final Callback exceptionCallback) { + super(ioSession, resultCallback, exceptionCallback); this.http2StreamHandlerFactory = Args.notNull(http2StreamHandlerFactory, "HTTP/2 stream handler factory"); this.strictALPNHandshake = strictALPNHandshake; this.initialized = new AtomicBoolean(); @@ -107,7 +110,7 @@ private void initialize() throws IOException { /** * @return true if the entire preface has been written out */ - private boolean writeOutPreface(final IOSession session, final ByteBuffer preface) throws IOException { + private boolean writeOutPreface(final IOSession session, final ByteBuffer preface) throws IOException { if (preface.hasRemaining()) { session.write(preface); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamHandler.java index 44271518c3..80e6db1d77 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamHandler.java @@ -30,6 +30,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; @@ -65,13 +66,12 @@ class ClientH2StreamHandler implements H2StreamHandler { private final AsyncClientExchangeHandler exchangeHandler; private final HandlerFactory pushHandlerFactory; private final HttpCoreContext context; + private final AtomicReference requestState; + private final AtomicReference responseState; private final AtomicBoolean requestCommitted; private final AtomicBoolean failed; private final AtomicBoolean done; - private volatile MessageState requestState; - private volatile MessageState responseState; - ClientH2StreamHandler( final H2StreamChannel outputChannel, final HttpProcessor httpProcessor, @@ -95,13 +95,13 @@ public int write(final ByteBuffer src) throws IOException { @Override public void endStream(final List trailers) throws IOException { outputChannel.endStream(trailers); - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); } @Override public void endStream() throws IOException { outputChannel.endStream(); - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); } }; @@ -113,8 +113,8 @@ public void endStream() throws IOException { this.requestCommitted = new AtomicBoolean(); this.failed = new AtomicBoolean(); this.done = new AtomicBoolean(); - this.requestState = MessageState.HEADERS; - this.responseState = MessageState.HEADERS; + this.requestState = new AtomicReference<>(MessageState.HEADERS); + this.responseState = new AtomicReference<>(MessageState.HEADERS); } @Override @@ -124,7 +124,7 @@ public HandlerFactory getPushHandlerFactory() { @Override public boolean isOutputReady() { - switch (requestState) { + switch (requestState.get()) { case HEADERS: return true; case BODY: @@ -142,19 +142,22 @@ private void commitRequest(final HttpRequest request, final EntityDetails entity httpProcessor.process(request, entityDetails, context); final List
headers = DefaultH2RequestConverter.INSTANCE.convert(request); - outputChannel.submit(headers, entityDetails == null); - connMetrics.incrementRequestCount(); - if (entityDetails == null) { - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); + outputChannel.submit(headers, true); + connMetrics.incrementRequestCount(); } else { + outputChannel.submit(headers, false); + connMetrics.incrementRequestCount(); final Header h = request.getFirstHeader(HttpHeaders.EXPECT); final boolean expectContinue = h != null && HeaderElements.CONTINUE.equalsIgnoreCase(h.getValue()); if (expectContinue) { - requestState = MessageState.ACK; + requestState.set(MessageState.ACK); } else { - requestState = MessageState.BODY; exchangeHandler.produce(dataChannel); + if (requestState.compareAndSet(MessageState.HEADERS, MessageState.BODY)) { + outputChannel.requestOutput(); + } } } } else { @@ -164,7 +167,7 @@ private void commitRequest(final HttpRequest request, final EntityDetails entity @Override public void produceOutput() throws HttpException, IOException { - switch (requestState) { + switch (requestState.get()) { case HEADERS: exchangeHandler.produceRequest((request, entityDetails, httpContext) -> commitRequest(request, entityDetails), context); break; @@ -184,7 +187,7 @@ public void consumeHeader(final List
headers, final boolean endStream) t if (done.get()) { throw new ProtocolException("Unexpected message headers"); } - switch (responseState) { + switch (responseState.get()) { case HEADERS: final HttpResponse response = DefaultH2ResponseConverter.INSTANCE.convert(headers); final int status = response.getCode(); @@ -194,9 +197,9 @@ public void consumeHeader(final List
headers, final boolean endStream) t if (status > HttpStatus.SC_CONTINUE && status < HttpStatus.SC_SUCCESS) { exchangeHandler.consumeInformation(response, context); } - if (requestState == MessageState.ACK) { + if (requestState.get() == MessageState.ACK) { if (status == HttpStatus.SC_CONTINUE || status >= HttpStatus.SC_SUCCESS) { - requestState = MessageState.BODY; + requestState.set(MessageState.BODY); exchangeHandler.produce(dataChannel); } } @@ -210,10 +213,10 @@ public void consumeHeader(final List
headers, final boolean endStream) t connMetrics.incrementResponseCount(); exchangeHandler.consumeResponse(response, entityDetails, context); - responseState = endStream ? MessageState.COMPLETE : MessageState.BODY; + responseState.set(endStream ? MessageState.COMPLETE : MessageState.BODY); break; case BODY: - responseState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); exchangeHandler.streamEnd(headers); break; default: @@ -228,14 +231,14 @@ public void updateInputCapacity() throws IOException { @Override public void consumeData(final ByteBuffer src, final boolean endStream) throws HttpException, IOException { - if (done.get() || responseState != MessageState.BODY) { + if (done.get() || responseState.get() != MessageState.BODY) { throw new ProtocolException("Unexpected message data"); } if (src != null) { exchangeHandler.consume(src); } if (endStream) { - responseState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); exchangeHandler.streamEnd(null); } } @@ -261,8 +264,8 @@ public void failed(final Exception cause) { @Override public void releaseResources() { if (done.compareAndSet(false, true)) { - responseState = MessageState.COMPLETE; - requestState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); + requestState.set(MessageState.COMPLETE); exchangeHandler.releaseResources(); } } @@ -270,8 +273,8 @@ public void releaseResources() { @Override public String toString() { return "[" + - "requestState=" + requestState + - ", responseState=" + responseState + + "requestState=" + requestState.get() + + ", responseState=" + responseState.get() + ']'; } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexer.java index b7850090b9..6737a7081c 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexer.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexer.java @@ -30,17 +30,18 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.http.config.CharCodingConfig; -import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.AsyncPushProducer; import org.apache.hc.core5.http.nio.HandlerFactory; -import org.apache.hc.core5.http.nio.command.ExecutableCommand; -import org.apache.hc.core5.http.nio.command.RequestExecutionCommand; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http2.H2ConnectionException; import org.apache.hc.core5.http2.H2Error; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.config.H2Param; +import org.apache.hc.core5.http2.config.H2Setting; import org.apache.hc.core5.http2.frame.DefaultFrameFactory; import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.http2.frame.StreamIdGenerator; @@ -87,13 +88,32 @@ public ClientH2StreamMultiplexer( this(ioSession, httpProcessor, null, h2Config, charCodingConfig); } + @Override + void validateSetting(final H2Param param, final int value) throws H2ConnectionException { + if (param == H2Param.ENABLE_PUSH && value == 1) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal ENABLE_PUSH setting"); + } + } + + @Override + H2Setting[] generateSettings(final H2Config localConfig) { + return new H2Setting[] { + new H2Setting(H2Param.HEADER_TABLE_SIZE, localConfig.getHeaderTableSize()), + new H2Setting(H2Param.ENABLE_PUSH, localConfig.isPushEnabled() ? 1 : 0), + new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()), + new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()), + new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()), + new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()) + }; + } + @Override void acceptHeaderFrame() throws H2ConnectionException { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal HEADERS frame"); } @Override - void acceptPushFrame() throws H2ConnectionException { + void acceptPushFrame() { } @Override @@ -102,37 +122,43 @@ void acceptPushRequest() throws H2ConnectionException { } @Override - H2StreamHandler createLocallyInitiatedStream( - final ExecutableCommand command, + H2Stream outgoingRequest( final H2StreamChannel channel, - final HttpProcessor httpProcessor, - final BasicHttpConnectionMetrics connMetrics) throws IOException { - if (command instanceof RequestExecutionCommand) { - final RequestExecutionCommand executionCommand = (RequestExecutionCommand) command; - final AsyncClientExchangeHandler exchangeHandler = executionCommand.getExchangeHandler(); - final HandlerFactory pushHandlerFactory = executionCommand.getPushHandlerFactory(); - final HttpCoreContext context = HttpCoreContext.castOrCreate(executionCommand.getContext()); - context.setSSLSession(getSSLSession()); - context.setEndpointDetails(getEndpointDetails()); - return new ClientH2StreamHandler(channel, httpProcessor, connMetrics, exchangeHandler, - pushHandlerFactory != null ? pushHandlerFactory : this.pushHandlerFactory, - context); - } - throw new H2ConnectionException(H2Error.INTERNAL_ERROR, "Unexpected executable command"); + final AsyncClientExchangeHandler exchangeHandler, + final HandlerFactory pushHandlerFactory, + final HttpContext context) { + final HttpCoreContext coreContext = HttpCoreContext.castOrCreate(context); + coreContext.setSSLSession(getSSLSession()); + coreContext.setEndpointDetails(getEndpointDetails()); + return new H2Stream(channel, new ClientH2StreamHandler(channel, getHttpProcessor(), getConnMetrics(), exchangeHandler, + pushHandlerFactory != null ? pushHandlerFactory : this.pushHandlerFactory, + coreContext)); } @Override - H2StreamHandler createRemotelyInitiatedStream( - final H2StreamChannel channel, - final HttpProcessor httpProcessor, - final BasicHttpConnectionMetrics connMetrics, - final HandlerFactory pushHandlerFactory) throws IOException { + H2Stream incomingRequest(final H2StreamChannel channel) throws IOException { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal incoming request"); + } + + @Override + H2Stream outgoingPushPromise(final H2StreamChannel channel, final AsyncPushProducer pushProducer) throws IOException { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal attempt to send push promise"); + } + + @Override + H2Stream incomingPushPromise(final H2StreamChannel channel, + final HandlerFactory pushHandlerFactory) { final HttpCoreContext context = HttpCoreContext.create(); context.setSSLSession(getSSLSession()); context.setEndpointDetails(getEndpointDetails()); - return new ClientPushH2StreamHandler(channel, httpProcessor, connMetrics, + return new H2Stream(channel, new ClientPushH2StreamHandler(channel, getHttpProcessor(), getConnMetrics(), pushHandlerFactory != null ? pushHandlerFactory : this.pushHandlerFactory, - context); + context), true); + } + + @Override + boolean allowGracefulAbort(final H2Stream stream) { + return stream.isRemoteClosed() && !stream.isLocalClosed(); } @Override diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexerFactory.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexerFactory.java index 574834a74c..c2b9afeea3 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexerFactory.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexerFactory.java @@ -36,6 +36,7 @@ import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.frame.DefaultFrameFactory; +import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.util.Args; @@ -53,25 +54,38 @@ public final class ClientH2StreamMultiplexerFactory { private final H2Config h2Config; private final CharCodingConfig charCodingConfig; private final H2StreamListener streamListener; + private final FrameFactory frameFactory; public ClientH2StreamMultiplexerFactory( final HttpProcessor httpProcessor, final HandlerFactory pushHandlerFactory, final H2Config h2Config, final CharCodingConfig charCodingConfig, - final H2StreamListener streamListener) { + final H2StreamListener streamListener, + final FrameFactory frameFactory) { this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); this.pushHandlerFactory = pushHandlerFactory; this.h2Config = h2Config != null ? h2Config : H2Config.DEFAULT; this.charCodingConfig = charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT; this.streamListener = streamListener; + this.frameFactory = frameFactory != null ? frameFactory : DefaultFrameFactory.INSTANCE; + } + + public ClientH2StreamMultiplexerFactory( + final HttpProcessor httpProcessor, + final HandlerFactory pushHandlerFactory, + final H2Config h2Config, + final CharCodingConfig charCodingConfig, + final H2StreamListener streamListener + ) { + this(httpProcessor, pushHandlerFactory, h2Config, charCodingConfig, streamListener, null); } public ClientH2StreamMultiplexerFactory( final HttpProcessor httpProcessor, final HandlerFactory pushHandlerFactory, final H2StreamListener streamListener) { - this(httpProcessor, pushHandlerFactory, null, null, streamListener); + this(httpProcessor, pushHandlerFactory, null, null, streamListener, null); } public ClientH2StreamMultiplexerFactory( @@ -81,7 +95,7 @@ public ClientH2StreamMultiplexerFactory( } public ClientH2StreamMultiplexer create(final ProtocolIOSession ioSession) { - return new ClientH2StreamMultiplexer(ioSession, DefaultFrameFactory.INSTANCE, httpProcessor, + return new ClientH2StreamMultiplexer(ioSession, frameFactory, httpProcessor, pushHandlerFactory, h2Config, charCodingConfig, streamListener); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2UpgradeHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2UpgradeHandler.java index 9009a9fe62..256929a8d3 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2UpgradeHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2UpgradeHandler.java @@ -33,6 +33,7 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.impl.nio.HttpConnectionEventHandler; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.reactor.ProtocolUpgradeHandler; @@ -49,15 +50,18 @@ public class ClientH2UpgradeHandler implements ProtocolUpgradeHandler { private final ClientH2StreamMultiplexerFactory http2StreamHandlerFactory; + private final Callback exceptionCallback; - public ClientH2UpgradeHandler(final ClientH2StreamMultiplexerFactory http2StreamHandlerFactory) { + public ClientH2UpgradeHandler(final ClientH2StreamMultiplexerFactory http2StreamHandlerFactory, + final Callback exceptionCallback) { this.http2StreamHandlerFactory = Args.notNull(http2StreamHandlerFactory, "HTTP/2 stream handler factory"); + this.exceptionCallback = exceptionCallback; } @Override public void upgrade(final ProtocolIOSession ioSession, final FutureCallback callback) { final HttpConnectionEventHandler protocolNegotiator = new ClientH2PrefaceHandler( - ioSession, http2StreamHandlerFactory, true, callback); + ioSession, http2StreamHandlerFactory, true, callback, exceptionCallback); ioSession.upgrade(protocolNegotiator); try { protocolNegotiator.connected(ioSession); diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java index a07f38a014..68f06d2b89 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java @@ -30,6 +30,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.nio.ClientHttp1IOEventHandler; import org.apache.hc.core5.http.impl.nio.ClientHttp1StreamDuplexerFactory; @@ -59,18 +60,21 @@ public class ClientHttpProtocolNegotiationStarter implements IOEventHandlerFacto private final HttpVersionPolicy versionPolicy; private final TlsStrategy tlsStrategy; private final Timeout handshakeTimeout; + private final Callback exceptionCallback; public ClientHttpProtocolNegotiationStarter( final ClientHttp1StreamDuplexerFactory http1StreamHandlerFactory, final ClientH2StreamMultiplexerFactory http2StreamHandlerFactory, final HttpVersionPolicy versionPolicy, final TlsStrategy tlsStrategy, - final Timeout handshakeTimeout) { + final Timeout handshakeTimeout, + final Callback exceptionCallback) { this.http1StreamHandlerFactory = Args.notNull(http1StreamHandlerFactory, "HTTP/1.1 stream handler factory"); this.http2StreamHandlerFactory = Args.notNull(http2StreamHandlerFactory, "HTTP/2 stream handler factory"); this.versionPolicy = versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE; this.tlsStrategy = tlsStrategy; this.handshakeTimeout = handshakeTimeout; + this.exceptionCallback = exceptionCallback; } @Override @@ -87,11 +91,11 @@ public HttpConnectionEventHandler createHandler(final ProtocolIOSession ioSessio } ioSession.registerProtocol(ApplicationProtocol.HTTP_1_1.id, new ClientHttp1UpgradeHandler(http1StreamHandlerFactory)); - ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ClientH2UpgradeHandler(http2StreamHandlerFactory)); + ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ClientH2UpgradeHandler(http2StreamHandlerFactory, exceptionCallback)); switch (endpointPolicy) { case FORCE_HTTP_2: - return new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, false); + return new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, false, exceptionCallback); case FORCE_HTTP_1: return new ClientHttp1IOEventHandler(http1StreamHandlerFactory.create(ioSession)); default: diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java index 8708b07233..fb1c8f2800 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/FrameInputBuffer.java @@ -30,7 +30,6 @@ import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; -import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http2.H2ConnectionException; import org.apache.hc.core5.http2.H2CorruptFrameException; import org.apache.hc.core5.http2.H2Error; @@ -61,6 +60,8 @@ enum State { HEAD_EXPECTED, PAYLOAD_EXPECTED } private int flags; private int streamId; + private boolean endOfStream; + FrameInputBuffer(final BasicH2TransportMetrics metrics, final int bufferLen, final int maxFramePayloadSize) { Args.notNull(metrics, "HTTP2 transport metrics"); Args.positive(maxFramePayloadSize, "Maximum payload size"); @@ -70,6 +71,7 @@ enum State { HEAD_EXPECTED, PAYLOAD_EXPECTED } this.buffer = ByteBuffer.wrap(bytes); this.buffer.flip(); this.state = State.HEAD_EXPECTED; + this.endOfStream = false; } public FrameInputBuffer(final BasicH2TransportMetrics metrics, final int maxFramePayloadSize) { @@ -174,11 +176,13 @@ public RawFrame read(final ByteBuffer src, final ReadableByteChannel channel) th } if (bytesRead == 0) { break; - } else if (bytesRead < 0) { + } + if (bytesRead == -1) { if (state != State.HEAD_EXPECTED || buffer.hasRemaining()) { throw new H2CorruptFrameException("Corrupt or incomplete HTTP2 frame"); } else { - throw new ConnectionClosedException(); + endOfStream = true; + break; } } } @@ -199,10 +203,18 @@ public RawFrame read(final ReadableByteChannel channel) throws IOException { public void reset() { buffer.compact(); state = State.HEAD_EXPECTED; + endOfStream = false; } public H2TransportMetrics getMetrics() { return metrics; } + /** + * @since 5.4 + */ + public boolean isEndOfStream() { + return endOfStream; + } + } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java new file mode 100644 index 0000000000..b7b991d311 --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Stream.java @@ -0,0 +1,229 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.impl.nio; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.StreamClosedException; +import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.HandlerFactory; +import org.apache.hc.core5.http2.H2Error; +import org.apache.hc.core5.http2.H2StreamResetException; + +class H2Stream { + + private static final long LINGER_TIME = 1000; // 1 second + + private final H2StreamChannel channel; + private final H2StreamHandler handler; + private final AtomicBoolean released; + + private volatile boolean reserved; + private volatile boolean remoteClosed; + + H2Stream(final H2StreamChannel channel, final H2StreamHandler handler, final boolean reserved) { + this.channel = channel; + this.handler = handler; + this.reserved = reserved; + this.released = new AtomicBoolean(); + } + + H2Stream(final H2StreamChannel channel, final H2StreamHandler handler) { + this(channel, handler, false); + } + + int getId() { + return channel.getId(); + } + + boolean isReserved() { + return reserved; + } + + void activate() { + reserved = false; + } + + AtomicInteger getOutputWindow() { + return channel.getOutputWindow(); + } + + AtomicInteger getInputWindow() { + return channel.getInputWindow(); + } + + private boolean isPastResetDeadline() { + final long localResetTime = channel.getLocalResetTime(); + return localResetTime > 0 && localResetTime + LINGER_TIME < System.currentTimeMillis(); + } + + boolean isTerminated() { + return channel.isLocalClosed() && (remoteClosed || isPastResetDeadline()); + } + + boolean isRemoteClosed() { + return remoteClosed; + } + + boolean isLocalClosed() { + return channel.isLocalClosed(); + } + + void consumePromise(final List
headers, final boolean endOfStream) throws HttpException, IOException { + try { + if (endOfStream) { + remoteClosed = true; + } + if (channel.isLocalReset()) { + return; + } + handler.consumePromise(headers); + channel.markLocalClosed(); + } catch (final ProtocolException ex) { + localReset(ex, H2Error.PROTOCOL_ERROR); + } + } + + void consumeHeader(final List
headers, final boolean endOfStream) throws HttpException, IOException { + try { + if (endOfStream) { + remoteClosed = true; + } + if (channel.isLocalReset()) { + return; + } + handler.consumeHeader(headers, remoteClosed); + } catch (final ProtocolException ex) { + localReset(ex, H2Error.PROTOCOL_ERROR); + } + } + + void consumeData(final ByteBuffer src, final boolean endOfStream) throws HttpException, IOException { + try { + if (endOfStream) { + remoteClosed = true; + } + if (channel.isLocalReset()) { + return; + } + handler.consumeData(src, remoteClosed); + } catch (final CharacterCodingException ex) { + localReset(ex, H2Error.INTERNAL_ERROR); + } catch (final ProtocolException ex) { + localReset(ex, H2Error.PROTOCOL_ERROR); + } + } + + boolean isOutputReady() { + return !reserved && !channel.isLocalClosed() && handler.isOutputReady(); + } + + void produceOutput() throws HttpException, IOException { + try { + handler.produceOutput(); + } catch (final ProtocolException ex) { + localReset(ex, H2Error.PROTOCOL_ERROR); + } + } + + void produceInputCapacityUpdate() throws IOException { + handler.updateInputCapacity(); + } + + void fail(final Exception cause) { + remoteClosed = true; + channel.markLocalClosed(); + if (released.compareAndSet(false, true)) { + handler.failed(cause); + handler.releaseResources(); + } + } + + void localReset(final Exception cause, final int code) throws IOException { + channel.localReset(code); + if (released.compareAndSet(false, true)) { + handler.failed(cause); + handler.releaseResources(); + } + } + + void localReset(final Exception cause, final H2Error error) throws IOException { + localReset(cause, error != null ? error.getCode() : H2Error.INTERNAL_ERROR.getCode()); + } + + void localReset(final H2StreamResetException ex) throws IOException { + localReset(ex, ex.getCode()); + } + + void handle(final HttpException ex) throws IOException, HttpException { + handler.handle(ex, remoteClosed); + } + + HandlerFactory getPushHandlerFactory() { + return handler.getPushHandlerFactory(); + } + + boolean abort() { + final boolean cancelled = channel.cancel(); + if (released.compareAndSet(false, true)) { + handler.failed(new StreamClosedException()); + handler.releaseResources(); + } + return cancelled; + } + + boolean abortGracefully() throws IOException { + if (!isLocalClosed() && isRemoteClosed()) { + channel.endStream(); + handler.releaseResources(); + return true; + } else { + return abort(); + } + } + + void releaseResources() { + if (released.compareAndSet(false, true)) { + handler.releaseResources(); + } + } + + @Override + public String toString() { + return channel.toString(); + } + +} diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2StreamChannel.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2StreamChannel.java index d200745c54..d86159ef9a 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2StreamChannel.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2StreamChannel.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.hc.core5.concurrent.Cancellable; import org.apache.hc.core5.http.Header; @@ -36,11 +37,41 @@ import org.apache.hc.core5.http.nio.AsyncPushProducer; import org.apache.hc.core5.http.nio.CapacityChannel; import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http2.H2Error; interface H2StreamChannel extends DataStreamChannel, CapacityChannel, Cancellable { + int getId(); + + AtomicInteger getOutputWindow(); + + AtomicInteger getInputWindow(); + void submit(List
headers, boolean endStream) throws HttpException, IOException; void push(List
headers, AsyncPushProducer pushProducer) throws HttpException, IOException; + boolean isLocalClosed(); + + void markLocalClosed(); + + boolean localReset(int errorCode) throws IOException; + + default boolean localReset(H2Error error) throws IOException { + return localReset(error != null ? error.getCode() : H2Error.INTERNAL_ERROR.getCode()); + } + + default void terminate() { + try { + localReset(H2Error.INTERNAL_ERROR); + } catch (final IOException ignore) { + } + } + + long getLocalResetTime(); + + default boolean isLocalReset() { + return getLocalResetTime() > 0; + } + } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Streams.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Streams.java new file mode 100644 index 0000000000..13724299b4 --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/H2Streams.java @@ -0,0 +1,174 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.impl.nio; + +import java.util.Iterator; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hc.core5.http.ConnectionClosedException; +import org.apache.hc.core5.http2.H2ConnectionException; +import org.apache.hc.core5.http2.H2Error; +import org.apache.hc.core5.http2.frame.StreamIdGenerator; +import org.apache.hc.core5.util.Args; + +class H2Streams { + + private final StreamIdGenerator idGenerator; + private final Map streamMap; + private final Queue streams; + private final AtomicInteger lastLocalId; + private final AtomicInteger lastRemoteId; + + public H2Streams(final StreamIdGenerator idGenerator) { + this.idGenerator = Args.notNull(idGenerator, "Stream id generator"); + this.streamMap = new ConcurrentHashMap<>(); + this.streams = new ConcurrentLinkedQueue<>(); + this.lastLocalId = new AtomicInteger(0); + this.lastRemoteId = new AtomicInteger(0); + } + + public int size() { + return streams.size(); + } + + public boolean isEmpty() { + return streams.isEmpty(); + } + + public Iterator iterator() { + return streams.iterator(); + } + + public int getLastLocalId() { + return lastLocalId.get(); + } + + public int getLastRemoteId() { + return lastRemoteId.get(); + } + + public void addLocallyInitiated(final H2Stream stream) throws H2ConnectionException { + final int streamId = stream.getId(); + if (isOtherSide(streamId)) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id"); + } + streamMap.put(streamId, stream); + streams.add(stream); + } + + public void addRemotelyInitiated(final H2Stream stream) throws H2ConnectionException { + final int streamId = stream.getId(); + if (isSameSide(streamId)) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id"); + } + final int currentId = lastRemoteId.get(); + if (streamId > currentId) { + lastRemoteId.compareAndSet(currentId, streamId); + } + streamMap.put(streamId, stream); + streams.add(stream); + } + + public void release(final H2Stream stream) { + streamMap.remove(stream.getId()); + stream.releaseResources(); + } + + public void shutdownAndReleaseAll() { + for (final Iterator it = streams.iterator(); it.hasNext(); ) { + final H2Stream stream = it.next(); + if (stream.isLocalClosed() && stream.isRemoteClosed()) { + stream.releaseResources(); + } else { + stream.fail(new ConnectionClosedException()); + } + } + streams.clear(); + streamMap.clear(); + } + + public H2Stream lookup(final int streamId) { + return streamMap.get(streamId); + } + + boolean hasBeenSeen(final int streamId) { + return streamId <= (isSameSide(streamId) ? lastLocalId : lastRemoteId).get(); + } + + boolean isClosed(final H2Stream stream, final int streamId) { + return stream != null ? stream.isLocalClosed() && stream.isRemoteClosed() : hasBeenSeen(streamId); + } + + public H2Stream lookupValidOrNull(final int streamId) throws H2ConnectionException { + final H2Stream stream = streamMap.get(streamId); + if (isClosed(stream, streamId)) { + throw new H2ConnectionException(H2Error.STREAM_CLOSED, "Stream closed"); + } + return stream; + } + + public H2Stream lookupSeen(final int streamId) throws H2ConnectionException { + final H2Stream stream = streamMap.get(streamId); + if (stream == null && !hasBeenSeen(streamId)) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected stream id: " + streamId); + } + return stream; + } + + public H2Stream lookupValid(final int streamId) throws H2ConnectionException { + final H2Stream stream = lookupValidOrNull(streamId); + if (stream == null) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected stream id: " + streamId); + } + return stream; + } + + public boolean isSameSide(final int streamId) { + return idGenerator.isSameSide(streamId); + } + + public boolean isOtherSide(final int streamId) { + return !idGenerator.isSameSide(streamId); + } + + public int generateStreamId() { + for (;;) { + final int currentId = lastLocalId.get(); + final int newStreamId = idGenerator.generate(currentId); + if (lastLocalId.compareAndSet(currentId, newStreamId)) { + return newStreamId; + } + } + } + + +} diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/HttpProtocolNegotiator.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/HttpProtocolNegotiator.java index 14f0bb409f..6a28a23427 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/HttpProtocolNegotiator.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/HttpProtocolNegotiator.java @@ -101,7 +101,7 @@ public void connected(final IOSession session) throws IOException { startProtocol(httpVersion); } @Override - public void inputReady(final IOSession session, final ByteBuffer src) throws IOException { + public void inputReady(final IOSession session, final ByteBuffer src) throws IOException { throw new ProtocolNegotiationException("Unexpected input"); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/PrefaceHandlerBase.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/PrefaceHandlerBase.java index fbe96d1ea5..3a5912b47b 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/PrefaceHandlerBase.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/PrefaceHandlerBase.java @@ -36,6 +36,7 @@ import javax.net.ssl.SSLSession; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.EndpointDetails; import org.apache.hc.core5.http.HttpVersion; @@ -55,14 +56,17 @@ abstract class PrefaceHandlerBase implements HttpConnectionEventHandler { final ProtocolIOSession ioSession; private final AtomicReference protocolHandlerRef; private final FutureCallback resultCallback; + private final Callback exceptionCallback; private final AtomicBoolean completed; PrefaceHandlerBase( final ProtocolIOSession ioSession, - final FutureCallback resultCallback) { + final FutureCallback resultCallback, + final Callback exceptionCallback) { this.ioSession = Args.notNull(ioSession, "I/O session"); this.protocolHandlerRef = new AtomicReference<>(); this.resultCallback = resultCallback; + this.exceptionCallback = exceptionCallback; this.completed = new AtomicBoolean(); } @@ -92,11 +96,17 @@ public void exception(final IOSession session, final Exception cause) { protocolHandler.exception(session, cause); } else { CommandSupport.failCommands(session, cause); + if (exceptionCallback != null) { + exceptionCallback.execute(cause); + } } } catch (final Exception ex) { if (completed.compareAndSet(false, true) && resultCallback != null) { resultCallback.failed(ex); } + if (exceptionCallback != null) { + exceptionCallback.execute(cause); + } } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2PrefaceHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2PrefaceHandler.java index 35c80c20a5..3807be94ad 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2PrefaceHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2PrefaceHandler.java @@ -32,6 +32,7 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.impl.nio.BufferedData; import org.apache.hc.core5.reactor.IOSession; @@ -54,15 +55,17 @@ public class ServerH2PrefaceHandler extends PrefaceHandlerBase { public ServerH2PrefaceHandler( final ProtocolIOSession ioSession, - final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory) { - this(ioSession, http2StreamHandlerFactory, null); + final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory, + final Callback exceptionCallback) { + this(ioSession, http2StreamHandlerFactory, null, exceptionCallback); } public ServerH2PrefaceHandler( final ProtocolIOSession ioSession, final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory, - final FutureCallback resultCallback) { - super(ioSession, resultCallback); + final FutureCallback resultCallback, + final Callback exceptionCallback) { + super(ioSession, resultCallback, exceptionCallback); this.http2StreamHandlerFactory = Args.notNull(http2StreamHandlerFactory, "HTTP/2 stream handler factory"); this.inBuf = BufferedData.allocate(1024); } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java index b754e8e67b..55316d0a94 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java @@ -30,6 +30,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; @@ -72,14 +73,14 @@ class ServerH2StreamHandler implements H2StreamHandler { private final BasicHttpConnectionMetrics connMetrics; private final HandlerFactory exchangeHandlerFactory; private final HttpCoreContext context; + private final AtomicReference requestState; + private final AtomicReference responseState; private final AtomicBoolean responseCommitted; private final AtomicBoolean failed; private final AtomicBoolean done; private volatile AsyncServerExchangeHandler exchangeHandler; private volatile HttpRequest receivedRequest; - private volatile MessageState requestState; - private volatile MessageState responseState; ServerH2StreamHandler( final H2StreamChannel outputChannel, @@ -103,13 +104,13 @@ public int write(final ByteBuffer src) throws IOException { @Override public void endStream(final List trailers) throws IOException { outputChannel.endStream(trailers); - responseState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); } @Override public void endStream() throws IOException { outputChannel.endStream(); - responseState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); } }; @@ -132,6 +133,11 @@ public void pushPromise( commitPromise(promise, pushProducer); } + @Override + public void terminateExchange() { + terminate(); + } + }; this.httpProcessor = httpProcessor; this.connMetrics = connMetrics; @@ -140,8 +146,8 @@ public void pushPromise( this.responseCommitted = new AtomicBoolean(); this.failed = new AtomicBoolean(); this.done = new AtomicBoolean(); - this.requestState = MessageState.HEADERS; - this.responseState = MessageState.IDLE; + this.requestState = new AtomicReference<>(MessageState.HEADERS); + this.responseState = new AtomicReference<>(MessageState.IDLE); } @Override @@ -176,14 +182,18 @@ private void commitResponse( final List
responseHeaders = DefaultH2ResponseConverter.INSTANCE.convert(response); final boolean endStream = responseEntityDetails == null || - (receivedRequest != null && Method.HEAD.isSame(receivedRequest.getMethod())); - outputChannel.submit(responseHeaders, endStream); - connMetrics.incrementResponseCount(); - if (responseEntityDetails == null) { - responseState = MessageState.COMPLETE; + receivedRequest != null && Method.HEAD.isSame(receivedRequest.getMethod()); + if (endStream) { + responseState.set(MessageState.COMPLETE); + outputChannel.submit(responseHeaders, endStream); + connMetrics.incrementResponseCount(); } else { - responseState = MessageState.BODY; + outputChannel.submit(responseHeaders, endStream); + connMetrics.incrementResponseCount(); exchangeHandler.produce(outputChannel); + if (responseState.compareAndSet(MessageState.IDLE, MessageState.BODY)) { + outputChannel.requestOutput(); + } } } else { throw new H2ConnectionException(H2Error.INTERNAL_ERROR, "Response already committed"); @@ -201,6 +211,10 @@ private void commitPromise( connMetrics.incrementRequestCount(); } + private void terminate() { + outputChannel.terminate(); + } + @Override public void consumePromise(final List
headers) throws HttpException, IOException { throw new ProtocolException("Unexpected message promise"); @@ -211,9 +225,9 @@ public void consumeHeader(final List
headers, final boolean endStream) t if (done.get()) { throw new ProtocolException("Unexpected message headers"); } - switch (requestState) { + switch (requestState.get()) { case HEADERS: - requestState = endStream ? MessageState.COMPLETE : MessageState.BODY; + requestState.set(endStream ? MessageState.COMPLETE : MessageState.BODY); final HttpRequest request = DefaultH2RequestConverter.INSTANCE.convert(headers); final EntityDetails requestEntityDetails = endStream ? null : new IncomingEntityDetails(request, -1); @@ -251,7 +265,7 @@ public void consumeHeader(final List
headers, final boolean endStream) t } break; case BODY: - responseState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); exchangeHandler.streamEnd(headers); break; default: @@ -267,7 +281,7 @@ public void updateInputCapacity() throws IOException { @Override public void consumeData(final ByteBuffer src, final boolean endStream) throws HttpException, IOException { - if (done.get() || requestState != MessageState.BODY) { + if (done.get() || requestState.get() != MessageState.BODY) { throw new ProtocolException("Unexpected message data"); } Asserts.notNull(exchangeHandler, "Exchange handler"); @@ -275,19 +289,19 @@ public void consumeData(final ByteBuffer src, final boolean endStream) throws Ht exchangeHandler.consume(src); } if (endStream) { - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); exchangeHandler.streamEnd(null); } } @Override public boolean isOutputReady() { - return responseState == MessageState.BODY && exchangeHandler != null && exchangeHandler.available() > 0; + return responseState.get() == MessageState.BODY && exchangeHandler != null && exchangeHandler.available() > 0; } @Override public void produceOutput() throws HttpException, IOException { - if (responseState == MessageState.BODY) { + if (responseState.get() == MessageState.BODY) { Asserts.notNull(exchangeHandler, "Exchange handler"); exchangeHandler.produce(dataChannel); } @@ -298,9 +312,9 @@ public void handle(final HttpException ex, final boolean endStream) throws HttpE if (done.get()) { throw ex; } - switch (requestState) { + switch (requestState.get()) { case HEADERS: - requestState = endStream ? MessageState.COMPLETE : MessageState.BODY; + requestState.set(endStream ? MessageState.COMPLETE : MessageState.BODY); if (!responseCommitted.get()) { final AsyncResponseProducer responseProducer = new BasicResponseProducer( ServerSupport.toStatusCode(ex), @@ -312,7 +326,7 @@ public void handle(final HttpException ex, final boolean endStream) throws HttpE } break; case BODY: - responseState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); default: throw ex; } @@ -334,8 +348,8 @@ public void failed(final Exception cause) { @Override public void releaseResources() { if (done.compareAndSet(false, true)) { - requestState = MessageState.COMPLETE; - responseState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); + responseState.set(MessageState.COMPLETE); if (exchangeHandler != null) { exchangeHandler.releaseResources(); } @@ -345,8 +359,8 @@ public void releaseResources() { @Override public String toString() { return "[" + - "requestState=" + requestState + - ", responseState=" + responseState + + "requestState=" + requestState.get() + + ", responseState=" + responseState.get() + ']'; } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexer.java index 2fe8f45273..ce68b027a1 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexer.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexer.java @@ -35,16 +35,19 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.RequestHeaderFieldsTooLargeException; import org.apache.hc.core5.http.config.CharCodingConfig; -import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.AsyncPushProducer; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.HandlerFactory; -import org.apache.hc.core5.http.nio.command.ExecutableCommand; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http2.H2ConnectionException; import org.apache.hc.core5.http2.H2Error; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.config.H2Param; +import org.apache.hc.core5.http2.config.H2Setting; import org.apache.hc.core5.http2.frame.DefaultFrameFactory; import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.http2.frame.StreamIdGenerator; @@ -86,11 +89,26 @@ public ServerH2StreamMultiplexer( } @Override - void acceptHeaderFrame() throws H2ConnectionException { + void validateSetting(final H2Param param, final int value) throws H2ConnectionException { } @Override - void acceptPushRequest() throws H2ConnectionException { + H2Setting[] generateSettings(final H2Config localConfig) { + return new H2Setting[] { + new H2Setting(H2Param.HEADER_TABLE_SIZE, localConfig.getHeaderTableSize()), + new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()), + new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()), + new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()), + new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()) + }; + } + + @Override + void acceptHeaderFrame() { + } + + @Override + void acceptPushRequest() { } @Override @@ -99,24 +117,42 @@ void acceptPushFrame() throws H2ConnectionException { } @Override - H2StreamHandler createRemotelyInitiatedStream( - final H2StreamChannel channel, - final HttpProcessor httpProcessor, - final BasicHttpConnectionMetrics connMetrics, - final HandlerFactory pushHandlerFactory) throws IOException { + H2Stream incomingRequest(final H2StreamChannel channel) { final HttpCoreContext context = HttpCoreContext.create(); context.setSSLSession(getSSLSession()); context.setEndpointDetails(getEndpointDetails()); - return new ServerH2StreamHandler(channel, httpProcessor, connMetrics, exchangeHandlerFactory, context); + return new H2Stream(channel, new ServerH2StreamHandler( + channel, getHttpProcessor(), getConnMetrics(), exchangeHandlerFactory, context)); } @Override - H2StreamHandler createLocallyInitiatedStream( - final ExecutableCommand command, + H2Stream outgoingRequest( final H2StreamChannel channel, - final HttpProcessor httpProcessor, - final BasicHttpConnectionMetrics connMetrics) throws IOException { - throw new H2ConnectionException(H2Error.INTERNAL_ERROR, "Illegal attempt to execute a request"); + final AsyncClientExchangeHandler exchangeHandler, + final HandlerFactory pushHandlerFactory, + final HttpContext context) throws IOException { + throw new H2ConnectionException(H2Error.INTERNAL_ERROR, "Illegal attempt to send a request"); + } + + @Override + H2Stream incomingPushPromise(final H2StreamChannel channel, + final HandlerFactory pushHandlerFactory) throws IOException { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal incoming push promise"); + } + + @Override + H2Stream outgoingPushPromise(final H2StreamChannel channel, + final AsyncPushProducer pushProducer) throws IOException { + final HttpCoreContext context = HttpCoreContext.create(); + context.setSSLSession(getSSLSession()); + context.setEndpointDetails(getEndpointDetails()); + return new H2Stream(channel, new ServerPushH2StreamHandler( + channel, getHttpProcessor(), getConnMetrics(), pushProducer, context), true); + } + + @Override + boolean allowGracefulAbort(final H2Stream stream) { + return false; } @Override diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexerFactory.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexerFactory.java index 38f0c0850a..49b3876845 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexerFactory.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamMultiplexerFactory.java @@ -36,6 +36,7 @@ import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.frame.DefaultFrameFactory; +import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.util.Args; @@ -53,24 +54,36 @@ public final class ServerH2StreamMultiplexerFactory { private final H2Config h2Config; private final CharCodingConfig charCodingConfig; private final H2StreamListener streamListener; + private final FrameFactory frameFactory; public ServerH2StreamMultiplexerFactory( final HttpProcessor httpProcessor, final HandlerFactory exchangeHandlerFactory, final H2Config h2Config, final CharCodingConfig charCodingConfig, - final H2StreamListener streamListener) { + final H2StreamListener streamListener, + final FrameFactory frameFactory) { this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); this.exchangeHandlerFactory = Args.notNull(exchangeHandlerFactory, "Exchange handler factory"); this.h2Config = h2Config != null ? h2Config : H2Config.DEFAULT; this.charCodingConfig = charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT; this.streamListener = streamListener; + this.frameFactory = frameFactory != null ? frameFactory : DefaultFrameFactory.INSTANCE; + } + + public ServerH2StreamMultiplexerFactory( + final HttpProcessor httpProcessor, + final HandlerFactory exchangeHandlerFactory, + final H2Config h2Config, + final CharCodingConfig charCodingConfig, + final H2StreamListener streamListener) { + this(httpProcessor, exchangeHandlerFactory, h2Config, charCodingConfig, streamListener, null); } public ServerH2StreamMultiplexer create(final ProtocolIOSession ioSession) { return new ServerH2StreamMultiplexer( ioSession, - DefaultFrameFactory.INSTANCE, + frameFactory, httpProcessor, exchangeHandlerFactory, charCodingConfig, diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2UpgradeHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2UpgradeHandler.java index 02b23f5575..90aee5c19b 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2UpgradeHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2UpgradeHandler.java @@ -33,6 +33,7 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.impl.nio.HttpConnectionEventHandler; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.reactor.ProtocolUpgradeHandler; @@ -49,15 +50,18 @@ public class ServerH2UpgradeHandler implements ProtocolUpgradeHandler { private final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory; + private final Callback exceptionCallback; - public ServerH2UpgradeHandler(final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory) { + public ServerH2UpgradeHandler(final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory, + final Callback exceptionCallback) { this.http2StreamHandlerFactory = Args.notNull(http2StreamHandlerFactory, "HTTP/2 stream handler factory"); + this.exceptionCallback = exceptionCallback; } @Override public void upgrade(final ProtocolIOSession ioSession, final FutureCallback callback) { final HttpConnectionEventHandler protocolNegotiator = new ServerH2PrefaceHandler( - ioSession, http2StreamHandlerFactory, callback); + ioSession, http2StreamHandlerFactory, callback, exceptionCallback); ioSession.upgrade(protocolNegotiator); try { protocolNegotiator.connected(ioSession); diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerHttpProtocolNegotiationStarter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerHttpProtocolNegotiationStarter.java index e9f1eac92e..1a29166d06 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerHttpProtocolNegotiationStarter.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerHttpProtocolNegotiationStarter.java @@ -30,6 +30,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.nio.HttpConnectionEventHandler; import org.apache.hc.core5.http.impl.nio.ServerHttp1IOEventHandler; @@ -59,18 +60,21 @@ public class ServerHttpProtocolNegotiationStarter implements IOEventHandlerFacto private final HttpVersionPolicy versionPolicy; private final TlsStrategy tlsStrategy; private final Timeout handshakeTimeout; + private final Callback exceptionCallback; public ServerHttpProtocolNegotiationStarter( final ServerHttp1StreamDuplexerFactory http1StreamHandlerFactory, final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory, final HttpVersionPolicy versionPolicy, final TlsStrategy tlsStrategy, - final Timeout handshakeTimeout) { + final Timeout handshakeTimeout, + final Callback exceptionCallback) { this.http1StreamHandlerFactory = Args.notNull(http1StreamHandlerFactory, "HTTP/1.1 stream handler factory"); this.http2StreamHandlerFactory = Args.notNull(http2StreamHandlerFactory, "HTTP/2 stream handler factory"); this.versionPolicy = versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE; this.tlsStrategy = tlsStrategy; this.handshakeTimeout = handshakeTimeout; + this.exceptionCallback = exceptionCallback; } @Override @@ -88,12 +92,14 @@ public HttpConnectionEventHandler createHandler(final ProtocolIOSession ioSessio } } - ioSession.registerProtocol(ApplicationProtocol.HTTP_1_1.id, new ServerHttp1UpgradeHandler(http1StreamHandlerFactory)); - ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ServerH2UpgradeHandler(http2StreamHandlerFactory)); + ioSession.registerProtocol(ApplicationProtocol.HTTP_1_1.id, + new ServerHttp1UpgradeHandler(http1StreamHandlerFactory)); + ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, + new ServerH2UpgradeHandler(http2StreamHandlerFactory, exceptionCallback)); switch (endpointPolicy) { case FORCE_HTTP_2: - return new ServerH2PrefaceHandler(ioSession, http2StreamHandlerFactory); + return new ServerH2PrefaceHandler(ioSession, http2StreamHandlerFactory, exceptionCallback); case FORCE_HTTP_1: return new ServerHttp1IOEventHandler(http1StreamHandlerFactory.create(uriScheme.id, ioSession)); default: diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerPushH2StreamHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerPushH2StreamHandler.java index e13bdf3769..6e4fc9d89b 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerPushH2StreamHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerPushH2StreamHandler.java @@ -195,6 +195,10 @@ private void commitPromise( connMetrics.incrementRequestCount(); } + private void terminate() { + outputChannel.terminate(); + } + @Override public void produceOutput() throws HttpException, IOException { switch (responseState) { @@ -219,6 +223,11 @@ public void pushPromise( commitPromise(promise, pushProducer); } + @Override + public void terminateExchange() { + terminate(); + } + }, context); break; case BODY: diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/FilterEntry.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/FilterEntry.java index 3d40c90555..1fadcffd3f 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/FilterEntry.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/FilterEntry.java @@ -29,7 +29,7 @@ class FilterEntry { - enum Position {BEFORE, AFTER, REPLACE, FIRST, LAST} + enum Position { BEFORE, AFTER, REPLACE, FIRST, LAST } final Position position; final String name; diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2AsyncRequester.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2AsyncRequester.java index e88a11f89c..0237a24cda 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2AsyncRequester.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2AsyncRequester.java @@ -44,8 +44,10 @@ import org.apache.hc.core5.pool.ManagedConnPool; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.IOWorkerSelector; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.reactor.ssl.TlsDetails; import org.apache.hc.core5.util.Timeout; @@ -64,24 +66,6 @@ public class H2AsyncRequester extends HttpAsyncRequester { * Use {@link H2RequesterBootstrap} to create instances of this class. */ @Internal - public H2AsyncRequester( - final HttpVersionPolicy versionPolicy, - final IOReactorConfig ioReactorConfig, - final IOEventHandlerFactory eventHandlerFactory, - final Decorator ioSessionDecorator, - final Callback exceptionCallback, - final IOSessionListener sessionListener, - final ManagedConnPool connPool) { - super(ioReactorConfig, eventHandlerFactory, ioSessionDecorator, exceptionCallback, sessionListener, connPool); - this.versionPolicy = versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE; - } - - /** - * Use {@link H2RequesterBootstrap} to create instances of this class. - * - * @since 5.2 - */ - @Internal public H2AsyncRequester( final HttpVersionPolicy versionPolicy, final IOReactorConfig ioReactorConfig, @@ -91,9 +75,11 @@ public H2AsyncRequester( final IOSessionListener sessionListener, final ManagedConnPool connPool, final TlsStrategy tlsStrategy, - final Timeout handshakeTimeout) { + final Timeout handshakeTimeout, + final IOReactorMetricsListener threadPoolListener, + final IOWorkerSelector workerSelector) { super(ioReactorConfig, eventHandlerFactory, ioSessionDecorator, exceptionCallback, sessionListener, connPool, - tlsStrategy, handshakeTimeout); + tlsStrategy, handshakeTimeout, threadPoolListener, workerSelector); this.versionPolicy = versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE; } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequester.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequester.java index b08eff9a87..88e2fb6d34 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequester.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequester.java @@ -37,9 +37,9 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.Cancellable; import org.apache.hc.core5.concurrent.CancellableDependency; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.concurrent.ComplexFuture; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Decorator; import org.apache.hc.core5.function.Resolver; @@ -70,8 +70,10 @@ import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.IOWorkerSelector; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; @@ -81,7 +83,7 @@ * * @since 5.0 */ -public class H2MultiplexingRequester extends AsyncRequester{ +public class H2MultiplexingRequester extends AsyncRequester { private final H2ConnPool connPool; @@ -96,9 +98,12 @@ public H2MultiplexingRequester( final Callback exceptionCallback, final IOSessionListener sessionListener, final Resolver addressResolver, - final TlsStrategy tlsStrategy) { + final TlsStrategy tlsStrategy, + final IOReactorMetricsListener threadPoolListener, + final IOWorkerSelector workerSelector) { super(eventHandlerFactory, ioReactorConfig, ioSessionDecorator, exceptionCallback, sessionListener, - ShutdownCommand.GRACEFUL_IMMEDIATE_CALLBACK, DefaultAddressResolver.INSTANCE); + ShutdownCommand.GRACEFUL_IMMEDIATE_CALLBACK, DefaultAddressResolver.INSTANCE, + threadPoolListener, workerSelector); this.connPool = new H2ConnPool(this, addressResolver, tlsStrategy); } @@ -175,7 +180,7 @@ private void execute( exchangeHandler.produceRequest((request, entityDetails, httpContext) -> { final HttpHost host = target != null ? target : defaultTarget(request); if (request.getAuthority() == null) { - request.setAuthority(new URIAuthority(host.getHostName(), host.getPort())); + request.setAuthority(new URIAuthority(host)); } connPool.getSession(host, timeout, new FutureCallback() { @@ -282,14 +287,7 @@ public final Future execute( final AsyncClientExchangeHandler exchangeHandler = new BasicClientExchangeHandler<>( requestProducer, responseConsumer, - new FutureContribution(future) { - - @Override - public void completed(final T result) { - future.completed(result); - } - - }); + new CompletingFutureContribution(future)); execute(target, exchangeHandler, pushHandlerFactory, future, timeout, context != null ? context : HttpCoreContext.create()); return future; } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequesterBootstrap.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequesterBootstrap.java index 207bea1984..a19e7913fc 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequesterBootstrap.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2MultiplexingRequesterBootstrap.java @@ -40,6 +40,7 @@ import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http.protocol.UriPatternType; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.http2.impl.H2Processors; import org.apache.hc.core5.http2.impl.nio.ClientH2PrefaceHandler; import org.apache.hc.core5.http2.impl.nio.ClientH2StreamMultiplexerFactory; @@ -47,6 +48,7 @@ import org.apache.hc.core5.http2.nio.support.DefaultAsyncPushConsumerFactory; import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; import org.apache.hc.core5.util.Args; @@ -70,6 +72,9 @@ public class H2MultiplexingRequesterBootstrap { private Callback exceptionCallback; private IOSessionListener sessionListener; private H2StreamListener streamListener; + private FrameFactory frameFactory; + + private IOReactorMetricsListener threadPoolListener; private H2MultiplexingRequesterBootstrap() { this.routeEntries = new ArrayList<>(); @@ -164,6 +169,17 @@ public final H2MultiplexingRequesterBootstrap setIOSessionListener(final IOSessi return this; } + /** + * Sets {@link IOReactorMetricsListener} instance. + * + * @return this instance. + * @since 5.4 + */ + public final H2MultiplexingRequesterBootstrap setIOReactorMetricsListener(final IOReactorMetricsListener threadPoolListener) { + this.threadPoolListener = threadPoolListener; + return this; + } + /** * Sets {@link H2StreamListener} instance. * @@ -174,6 +190,17 @@ public final H2MultiplexingRequesterBootstrap setStreamListener(final H2StreamLi return this; } + /** + * Sets {@link FrameFactory} instance. + * + * @since 5.4 + * @return this instance. + */ + public final H2MultiplexingRequesterBootstrap setStreamListener(final FrameFactory frameFactory) { + this.frameFactory = frameFactory; + return this; + } + /** * Sets {@link UriPatternType} for handler registration. * @@ -235,15 +262,19 @@ public H2MultiplexingRequester create() { new DefaultAsyncPushConsumerFactory(requestRouter), h2Config != null ? h2Config : H2Config.DEFAULT, charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT, - streamListener); + streamListener, + frameFactory); return new H2MultiplexingRequester( ioReactorConfig, - (ioSession, attachment) -> new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, strictALPNHandshake), + (ioSession, attachment) -> + new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, strictALPNHandshake, exceptionCallback), ioSessionDecorator, exceptionCallback, sessionListener, DefaultAddressResolver.INSTANCE, - tlsStrategy != null ? tlsStrategy : new H2ClientTlsStrategy()); + tlsStrategy != null ? tlsStrategy : new H2ClientTlsStrategy(), + threadPoolListener, + null); } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2RequesterBootstrap.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2RequesterBootstrap.java index c0ea93a0d7..9d0f767d01 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2RequesterBootstrap.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2RequesterBootstrap.java @@ -50,6 +50,7 @@ import org.apache.hc.core5.http.protocol.UriPatternType; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.http2.impl.H2Processors; import org.apache.hc.core5.http2.impl.nio.ClientH2StreamMultiplexerFactory; import org.apache.hc.core5.http2.impl.nio.ClientHttpProtocolNegotiationStarter; @@ -65,6 +66,7 @@ import org.apache.hc.core5.pool.StrictConnPool; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; import org.apache.hc.core5.util.Args; @@ -99,6 +101,9 @@ public class H2RequesterBootstrap { private H2StreamListener streamListener; private Http1StreamListener http1StreamListener; private ConnPoolListener connPoolListener; + private IOReactorMetricsListener threadPoolListener; + private FrameFactory frameFactory; + private H2RequesterBootstrap() { this.routeEntries = new ArrayList<>(); @@ -249,6 +254,17 @@ public final H2RequesterBootstrap setIOSessionListener(final IOSessionListener s return this; } + /** + * Sets {@link IOReactorMetricsListener} instance. + * + * @return this instance. + * @since 5.4 + */ + public final H2RequesterBootstrap setIOReactorMetricsListener(final IOReactorMetricsListener threadPoolListener) { + this.threadPoolListener = threadPoolListener; + return this; + } + /** * Sets {@link H2StreamListener} instance. * @@ -289,6 +305,17 @@ public final H2RequesterBootstrap setUriPatternType(final UriPatternType uriPatt return this; } + /** + * Sets {@link FrameFactory} instance. + * + * @since 5.4 + * @return this instance. + */ + public final H2RequesterBootstrap setFrameFactory(final FrameFactory frameFactory) { + this.frameFactory = frameFactory; + return this; + } + /** * Registers the given {@link AsyncPushConsumer} {@link Supplier} as a default handler for URIs * matching the given pattern. @@ -362,7 +389,8 @@ public H2AsyncRequester create() { new DefaultAsyncPushConsumerFactory(requestRouter), h2Config != null ? h2Config : H2Config.DEFAULT, charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT, - streamListener); + streamListener, + frameFactory); final TlsStrategy actualTlsStrategy = tlsStrategy != null ? tlsStrategy : new H2ClientTlsStrategy(); @@ -382,7 +410,8 @@ public H2AsyncRequester create() { http2StreamHandlerFactory, versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE, actualTlsStrategy, - handshakeTimeout); + handshakeTimeout, + exceptionCallback); return new H2AsyncRequester( versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE, @@ -393,7 +422,9 @@ public H2AsyncRequester create() { sessionListener, connPool, actualTlsStrategy, - handshakeTimeout); + handshakeTimeout, + threadPoolListener, + null); } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java index 822cbe7f4a..1d2aab3ce9 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/bootstrap/H2ServerBootstrap.java @@ -62,6 +62,7 @@ import org.apache.hc.core5.http.protocol.UriPatternType; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.http2.impl.H2Processors; import org.apache.hc.core5.http2.impl.nio.H2StreamListener; import org.apache.hc.core5.http2.impl.nio.ServerH2StreamMultiplexerFactory; @@ -71,6 +72,7 @@ import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; import org.apache.hc.core5.util.Args; @@ -102,6 +104,8 @@ public class H2ServerBootstrap { private IOSessionListener sessionListener; private H2StreamListener h2StreamListener; private Http1StreamListener http1StreamListener; + private IOReactorMetricsListener threadPoolListener; + private FrameFactory frameFactory; private H2ServerBootstrap() { this.routeEntries = new ArrayList<>(); @@ -208,6 +212,18 @@ public final H2ServerBootstrap setIOSessionDecorator(final Decorator return this; } + + /** + * Sets {@link IOReactorMetricsListener} instance. + * + * @return this instance. + * @since 5.4 + */ + public final H2ServerBootstrap setIOReactorMetricsListener(final IOReactorMetricsListener threadPoolListener) { + this.threadPoolListener = threadPoolListener; + return this; + } + /** * Sets {@link Exception} {@link Callback} instance. * @@ -238,6 +254,17 @@ public final H2ServerBootstrap setStreamListener(final H2StreamListener h2Stream return this; } + /** + * Sets {@link FrameFactory} instance. + * + * @since 5.4 + * @return this instance. + */ + public final H2ServerBootstrap setFrameFactory(final FrameFactory frameFactory) { + this.frameFactory = frameFactory; + return this; + } + /** * Sets {@link Http1StreamListener} instance. * @@ -247,7 +274,6 @@ public final H2ServerBootstrap setStreamListener(final Http1StreamListener http1 this.http1StreamListener = http1StreamListener; return this; } - /** * @return this instance. * @deprecated Use {@link RequestRouter}. @@ -498,7 +524,8 @@ public HttpAsyncServer create() { handlerFactory, h2Config != null ? h2Config : H2Config.DEFAULT, charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT, - h2StreamListener); + h2StreamListener, + frameFactory); final TlsStrategy actualTlsStrategy = tlsStrategy != null ? tlsStrategy : new H2ServerTlsStrategy(); @@ -512,17 +539,19 @@ public HttpAsyncServer create() { new DefaultHttpResponseWriterFactory(http1Config), DefaultContentLengthStrategy.INSTANCE, DefaultContentLengthStrategy.INSTANCE, - http1StreamListener); + http1StreamListener, + exceptionCallback); final IOEventHandlerFactory ioEventHandlerFactory = new ServerHttpProtocolNegotiationStarter( http1StreamHandlerFactory, http2StreamHandlerFactory, versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE, actualTlsStrategy, - handshakeTimeout); + handshakeTimeout, + exceptionCallback); return new HttpAsyncServer(ioEventHandlerFactory, ioReactorConfig, ioSessionDecorator, exceptionCallback, - sessionListener, actualCanonicalHostName); + sessionListener, threadPoolListener, null, actualCanonicalHostName); } } diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/nio/command/PushResponseCommand.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/nio/command/PushResponseCommand.java new file mode 100644 index 0000000000..c5b22997fb --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/nio/command/PushResponseCommand.java @@ -0,0 +1,65 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.nio.command; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.reactor.Command; +import org.apache.hc.core5.util.Args; + +/** + * Activates the stream reserved with a push promise + * + * @since 5.4 + */ +@Internal +public final class PushResponseCommand implements Command { + + private final int streamId; + private final AtomicBoolean cancelled; + + public PushResponseCommand(final int streamId) { + this.streamId = Args.positive(streamId, "Stream Id"); + this.cancelled = new AtomicBoolean(); + } + + public int getStreamId() { + return streamId; + } + + public boolean isCancelled() { + return cancelled.get(); + } + + @Override + public boolean cancel() { + return cancelled.compareAndSet(false, true); + } + +} diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java new file mode 100644 index 0000000000..ef528fbc29 --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java @@ -0,0 +1,94 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.protocol; + +import java.io.IOException; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; + +/** + * This request interceptor is responsible for execution of the protocol conformance + * checks on incoming or outgoing HTTP/2 request messages. + * + * @since 5.4 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class H2RequestConformance implements HttpRequestInterceptor { + + public static final H2RequestConformance INSTANCE = new H2RequestConformance(); + + private final String[] illegalHeaderNames; + + @Internal + public H2RequestConformance(final String... illegalHeaderNames) { + super(); + this.illegalHeaderNames = illegalHeaderNames; + } + + public H2RequestConformance() { + this( + HttpHeaders.CONNECTION, + HttpHeaders.KEEP_ALIVE, + HttpHeaders.PROXY_CONNECTION, + HttpHeaders.TRANSFER_ENCODING, + HttpHeaders.UPGRADE, + HttpHeaders.TE); + } + + @Override + public void process(final HttpRequest request, final EntityDetails entity, final HttpContext localContext) + throws HttpException, IOException { + Args.notNull(request, "HTTP request"); + for (int i = 0; i < illegalHeaderNames.length; i++) { + final String headerName = illegalHeaderNames[i]; + final Header header = request.getFirstHeader(headerName); + if (header != null) { + if (headerName.equalsIgnoreCase(HttpHeaders.TE)) { + final String value = header.getValue(); + if (!"trailers".equalsIgnoreCase(value)) { + throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", HttpHeaders.TE, value); + } + } else { + throw new ProtocolException("Header '%s' is illegal for HTTP/2 messages", headerName); + } + } + } + } + +} diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2ResponseConformance.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2ResponseConformance.java new file mode 100644 index 0000000000..61e9dc3bb4 --- /dev/null +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2ResponseConformance.java @@ -0,0 +1,85 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.protocol; + +import java.io.IOException; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpResponseInterceptor; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; + +/** + * This response interceptor is responsible for making the protocol conformance checks + * of incoming or outgoing HTTP/2 response messages. + * + * @since 5.4 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class H2ResponseConformance implements HttpResponseInterceptor { + + public static final H2ResponseConformance INSTANCE = new H2ResponseConformance(); + + private final String[] illegalHeaderNames; + + @Internal + public H2ResponseConformance(final String... illegalHeaderNames) { + super(); + this.illegalHeaderNames = illegalHeaderNames; + } + + public H2ResponseConformance() { + this( + HttpHeaders.CONNECTION, + HttpHeaders.KEEP_ALIVE, + HttpHeaders.TRANSFER_ENCODING, + HttpHeaders.UPGRADE); + } + + @Override + public void process(final HttpResponse response, final EntityDetails entity, final HttpContext context) + throws HttpException, IOException { + Args.notNull(response, "HTTP response"); + for (int i = 0; i < illegalHeaderNames.length; i++) { + final String headerName = illegalHeaderNames[i]; + final Header header = response.getFirstHeader(headerName); + if (header != null) { + throw new ProtocolException("Header '%s' is illegal for HTTP/2 messages", headerName); + } + } + } + +} diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/ssl/ConscryptServerTlsStrategy.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/ssl/ConscryptServerTlsStrategy.java index d5557580be..375c32a11e 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/ssl/ConscryptServerTlsStrategy.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/ssl/ConscryptServerTlsStrategy.java @@ -148,7 +148,7 @@ public ConscryptServerTlsStrategy(final SSLContext sslContext) { * @since 5.2 */ public ConscryptServerTlsStrategy() { - this(SSLContexts.createSystemDefault(), (SSLBufferMode) null, null, null); + this(SSLContexts.createSystemDefault(), (SSLBufferMode) null, null, null); } /** diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/ClassicH2RequestExecutionExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/ClassicH2RequestExecutionExample.java new file mode 100644 index 0000000000..dedc4056df --- /dev/null +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/ClassicH2RequestExecutionExample.java @@ -0,0 +1,155 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http2.examples; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.Future; + +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpConnection; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncRequestProducer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncResponseConsumer; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.frame.RawFrame; +import org.apache.hc.core5.http2.impl.nio.H2StreamListener; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.util.Timeout; + +/** + * Example of HTTP/2 request execution with a classic I/O API compatibility bridge + * that enables the use of standard {@link java.io.InputStream} / {@link java.io.OutputStream} + * based data consumers / producers. + *

> + * Execution of individual message exchanges is performed at the current thread. + */ +@Experimental +public class ClassicH2RequestExecutionExample { + + public static void main(final String[] args) throws Exception { + + // Create and start requester + final H2Config h2Config = H2Config.custom() + .setPushEnabled(false) + .build(); + + final HttpAsyncRequester requester = H2RequesterBootstrap.bootstrap() + .setH2Config(h2Config) + .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2) + .setStreamListener(new H2StreamListener() { + + @Override + public void onHeaderInput(final HttpConnection connection, final int streamId, final List headers) { + for (int i = 0; i < headers.size(); i++) { + System.out.println(connection.getRemoteAddress() + " (" + streamId + ") << " + headers.get(i)); + } + } + + @Override + public void onHeaderOutput(final HttpConnection connection, final int streamId, final List headers) { + for (int i = 0; i < headers.size(); i++) { + System.out.println(connection.getRemoteAddress() + " (" + streamId + ") >> " + headers.get(i)); + } + } + + @Override + public void onFrameInput(final HttpConnection connection, final int streamId, final RawFrame frame) { + } + + @Override + public void onFrameOutput(final HttpConnection connection, final int streamId, final RawFrame frame) { + } + + @Override + public void onInputFlowControl(final HttpConnection connection, final int streamId, final int delta, final int actualSize) { + } + + @Override + public void onOutputFlowControl(final HttpConnection connection, final int streamId, final int delta, final int actualSize) { + } + + }) + .create(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("HTTP requester shutting down"); + requester.close(CloseMode.GRACEFUL); + })); + requester.start(); + + final HttpHost target = new HttpHost("nghttp2.org"); + final Future future = requester.connect(target, Timeout.ofDays(5)); + final AsyncClientEndpoint clientEndpoint = future.get(); + + final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; + + for (final String requestUri: requestUris) { + final ClassicHttpRequest request = ClassicRequestBuilder.get() + .setHttpHost(target) + .setPath(requestUri) + .build(); + + final ClassicToAsyncRequestProducer requestProducer = new ClassicToAsyncRequestProducer(request, Timeout.ofMinutes(5)); + final ClassicToAsyncResponseConsumer responseConsumer = new ClassicToAsyncResponseConsumer(Timeout.ofMinutes(5)); + + clientEndpoint.execute(requestProducer, responseConsumer, null); + + requestProducer.blockWaiting().execute(); + try (ClassicHttpResponse response = responseConsumer.blockWaiting()) { + System.out.println(requestUri + " -> " + response.getCode()); + final HttpEntity entity = response.getEntity(); + if (entity != null) { + final ContentType contentType = ContentType.parse(entity.getContentType()); + final Charset charset = ContentType.getCharset(contentType, StandardCharsets.UTF_8); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), charset))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } + } + } + } + + System.out.println("Shutting down I/O reactor"); + requester.initiateShutdown(); + } + +} diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/ClassicH2ServerExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/ClassicH2ServerExample.java new file mode 100644 index 0000000000..d3d2607580 --- /dev/null +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/ClassicH2ServerExample.java @@ -0,0 +1,188 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http2.examples; + +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.concurrent.DefaultThreadFactory; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpConnection; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.entity.EntityTemplate; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncServerExchangeHandler; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.frame.RawFrame; +import org.apache.hc.core5.http2.impl.nio.H2StreamListener; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.net.URIBuilder; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.util.TimeValue; + +/** + * Example of asynchronous embedded HTTP/2 server with a classic I/O API compatibility + * bridge that enables the use of standard {@link java.io.InputStream} / {@link java.io.OutputStream} + * based data consumers / producers. + *

> + * Execution of individual message exchanges is delegated to an {@link java.util.concurrent.Executor} + * backed by a pool of threads. + */ +@Experimental +public class ClassicH2ServerExample { + + public static void main(final String[] args) throws Exception { + int port = 8080; + if (args.length >= 1) { + port = Integer.parseInt(args[0]); + } + + final IOReactorConfig config = IOReactorConfig.custom() + .setSoTimeout(15, TimeUnit.SECONDS) + .setTcpNoDelay(true) + .build(); + + final ExecutorService executorService = Executors.newFixedThreadPool( + 25, + new DefaultThreadFactory("worker-pool", true)); + + final HttpRequestHandler requestHandler = (request, response, context) -> { + try { + final HttpEntity requestEntity = request.getEntity(); + if (requestEntity != null) { + EntityUtils.consume(requestEntity); + } + final Map queryParams = new URIBuilder(request.getUri()).getQueryParams().stream() + .collect(Collectors.toMap( + NameValuePair::getName, + NameValuePair::getValue, + (s, s2) -> s)); + final int n = Integer.parseInt(queryParams.getOrDefault("n", "10")); + final String p = queryParams.getOrDefault("pattern", "huh?"); + final HttpEntity responseEntity = new EntityTemplate( + ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8), + outputStream -> { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) { + for (int i = 0; i < n; i++) { + writer.write(p); + writer.write("\n"); + } + } + }); + response.setEntity(responseEntity); + } catch (final URISyntaxException ex) { + throw new ProtocolException("Invalid request URI", ex); + } catch (final NumberFormatException ex) { + throw new ProtocolException("Invalid query parameter", ex); + } + }; + + final RequestRouter requestRouter = RequestRouter.builder() + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", requestHandler) + .build(); + + final HttpAsyncServer server = H2ServerBootstrap.bootstrap() + .setIOReactorConfig(config) + .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2) + .setStreamListener(new H2StreamListener() { + + @Override + public void onHeaderInput(final HttpConnection connection, final int streamId, final List headers) { + for (int i = 0; i < headers.size(); i++) { + System.out.println(connection.getRemoteAddress() + " (" + streamId + ") << " + headers.get(i)); + } + } + + @Override + public void onHeaderOutput(final HttpConnection connection, final int streamId, final List headers) { + for (int i = 0; i < headers.size(); i++) { + System.out.println(connection.getRemoteAddress() + " (" + streamId + ") >> " + headers.get(i)); + } + } + + @Override + public void onFrameInput(final HttpConnection connection, final int streamId, final RawFrame frame) { + } + + @Override + public void onFrameOutput(final HttpConnection connection, final int streamId, final RawFrame frame) { + } + + @Override + public void onInputFlowControl(final HttpConnection connection, final int streamId, final int delta, final int actualSize) { + } + + @Override + public void onOutputFlowControl(final HttpConnection connection, final int streamId, final int delta, final int actualSize) { + } + + }) + .setRequestRouter((request, context) -> { + final HttpRequestHandler handler = requestRouter.resolve(request, context); + return () -> new ClassicToAsyncServerExchangeHandler( + executorService, + handler, + e -> e.printStackTrace(System.out)); + }) + .setExceptionCallback(e -> e.printStackTrace(System.out)) + .create(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("HTTP server shutting down"); + server.close(CloseMode.GRACEFUL); + executorService.shutdownNow(); + })); + + server.start(); + final Future future = server.listen(new InetSocketAddress(port), URIScheme.HTTP); + final ListenerEndpoint listenerEndpoint = future.get(); + System.out.print("Listening on " + listenerEndpoint.getAddress()); + server.awaitShutdown(TimeValue.ofDays(Long.MAX_VALUE)); + } + +} diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java index e33f4e248a..8edea1d6be 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java @@ -114,12 +114,13 @@ public void onOutputFlowControl(final HttpConnection connection, final int strea requester.start(); final HttpHost target = new HttpHost("https", "nghttp2.org", 443); + final Future future = requester.connect(target, Timeout.ofDays(5)); + final AsyncClientEndpoint clientEndpoint = future.get(); + final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; final CountDownLatch latch = new CountDownLatch(requestUris.length); for (final String requestUri: requestUris) { - final Future future = requester.connect(target, Timeout.ofDays(5)); - final AsyncClientEndpoint clientEndpoint = future.get(); clientEndpoint.execute( AsyncRequestBuilder.get() .setHttpHost(target) diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java index 3653baeebe..4df72137f8 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java @@ -105,12 +105,13 @@ public void onOutputFlowControl(final HttpConnection connection, final int strea requester.start(); final HttpHost target = new HttpHost("nghttp2.org"); + final Future future = requester.connect(target, Timeout.ofSeconds(5)); + final AsyncClientEndpoint clientEndpoint = future.get(); + final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; final CountDownLatch latch = new CountDownLatch(requestUris.length); for (final String requestUri: requestUris) { - final Future future = requester.connect(target, Timeout.ofSeconds(5)); - final AsyncClientEndpoint clientEndpoint = future.get(); clientEndpoint.execute( AsyncRequestBuilder.get() .setHttpHost(target) diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/hpack/TestHPackCoding.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/hpack/TestHPackCoding.java index dba042bd29..b93ed6084a 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/hpack/TestHPackCoding.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/hpack/TestHPackCoding.java @@ -210,8 +210,8 @@ void testHuffmanEncoding() { @Test void testBasicStringCoding() throws Exception { - final HPackEncoder encoder = new HPackEncoder(StandardCharsets.US_ASCII); - final HPackDecoder decoder = new HPackDecoder(StandardCharsets.US_ASCII); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); final ByteArrayBuffer buffer = new ByteArrayBuffer(16); encoder.encodeString(buffer, "this and that", false); @@ -230,8 +230,8 @@ void testBasicStringCoding() throws Exception { @Test void testEnsureCapacity() throws Exception { - final HPackEncoder encoder = new HPackEncoder(StandardCharsets.US_ASCII); - final HPackDecoder decoder = new HPackDecoder(StandardCharsets.UTF_8); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, StandardCharsets.UTF_8); final ByteArrayBuffer buffer = new ByteArrayBuffer(16); encoder.encodeString(buffer, "this and that", false); @@ -274,8 +274,8 @@ void testComplexStringCoding1() throws Exception { final ByteArrayBuffer buffer = new ByteArrayBuffer(16); final StringBuilder strBuf = new StringBuilder(); - final HPackEncoder encoder = new HPackEncoder(charset); - final HPackDecoder decoder = new HPackDecoder(charset); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, charset); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, charset); for (int n = 0; n < 10; n++) { @@ -302,8 +302,8 @@ void testComplexStringCoding2() throws Exception { final ByteArrayBuffer buffer = new ByteArrayBuffer(16); final StringBuilder strBuf = new StringBuilder(); - final HPackEncoder encoder = new HPackEncoder(charset); - final HPackDecoder decoder = new HPackDecoder(charset); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, charset); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, charset); for (int n = 0; n < 10; n++) { @@ -1061,8 +1061,8 @@ void testHeaderEntrySizeNonAscii() throws Exception { @Test void testHeaderSizeLimit() throws Exception { - final HPackEncoder encoder = new HPackEncoder(StandardCharsets.US_ASCII); - final HPackDecoder decoder = new HPackDecoder(StandardCharsets.US_ASCII); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); final ByteArrayBuffer buf = new ByteArrayBuffer(128); @@ -1086,8 +1086,8 @@ void testHeaderSizeLimit() throws Exception { @Test void testHeaderEmptyASCII() throws Exception { - final HPackEncoder encoder = new HPackEncoder(StandardCharsets.US_ASCII); - final HPackDecoder decoder = new HPackDecoder(StandardCharsets.US_ASCII); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, StandardCharsets.US_ASCII); final ByteArrayBuffer buf = new ByteArrayBuffer(128); @@ -1100,8 +1100,8 @@ void testHeaderEmptyASCII() throws Exception { @Test void testHeaderEmptyUTF8() throws Exception { - final HPackEncoder encoder = new HPackEncoder(StandardCharsets.UTF_8); - final HPackDecoder decoder = new HPackDecoder(StandardCharsets.UTF_8); + final HPackEncoder encoder = new HPackEncoder(Integer.MAX_VALUE, StandardCharsets.UTF_8); + final HPackDecoder decoder = new HPackDecoder(Integer.MAX_VALUE, StandardCharsets.UTF_8); final ByteArrayBuffer buf = new ByteArrayBuffer(128); @@ -1111,5 +1111,74 @@ void testHeaderEmptyUTF8() throws Exception { assertHeaderEquals(header, decoder.decodeHeader(wrap(buf))); } + @Test + void encoderDynamicHeaderTableMaxSizeNotIncreasedBySettingsFrame() throws Exception { + final OutboundDynamicTable dynamicTable = new OutboundDynamicTable(4096); + final HPackEncoder encoder = new HPackEncoder(dynamicTable, StandardCharsets.UTF_8); + //emulate receiving a settings frame from the receiver + encoder.setMaxTableSize(Integer.MAX_VALUE); + //actual table size should not change until we are able to send an update to the receiver + Assertions.assertEquals(4096, dynamicTable.getMaxSize()); + } + + @Test + void encoderDynamicHeaderTableMaxSizeChangeCausesUpdateHeader() throws Exception { + final OutboundDynamicTable dynamicTable = new OutboundDynamicTable(4096); + final HPackEncoder encoder = new HPackEncoder(dynamicTable, StandardCharsets.UTF_8); + //emulate receiving a settings frame from the receiver + encoder.setMaxTableSize(8192); + + final ByteArrayBuffer buf = new ByteArrayBuffer(128); + + final Header header = new BasicHeader("empty-header", ""); + encoder.encodeHeader(buf, header); + + final int firstByte = buf.byteAt(0); + final int masked = firstByte & 0xe0; + + //first header field is table size update + Assertions.assertEquals(0x20, masked); + + //update has new table size as value + Assertions.assertEquals(8192, HPackDecoder.decodeInt(wrap(buf), 5)); + } + + @Test + void decoderDynamicHeaderTableMaxSizeNotIncreasedBySettingsFrame() throws Exception { + final InboundDynamicTable dynamicTable = new InboundDynamicTable(4096); + final HPackDecoder decoder = new HPackDecoder(dynamicTable, StandardCharsets.UTF_8); + //emulate something on our side changing the max dynamic table size + //this would cause us to send a new settings frame to the sender + decoder.setMaxTableSize(Integer.MAX_VALUE); + //actual table size should not change until sender sends us a table update + Assertions.assertEquals(4096, dynamicTable.getMaxSize()); + } + + @Test + void decoderDynamicHeaderTableMaxSizeUpdatesAfter() throws Exception { + final InboundDynamicTable dynamicTable = new InboundDynamicTable(4096); + final HPackDecoder decoder = new HPackDecoder(dynamicTable, StandardCharsets.UTF_8); + decoder.setMaxTableSize(Integer.MAX_VALUE); + + final ByteArrayBuffer buf = new ByteArrayBuffer(128); + HPackEncoder.encodeInt(buf, 5, 8192, 0x20); + + decoder.decodeHeaders(wrap(buf)); + + Assertions.assertEquals(8192, dynamicTable.getMaxSize()); + } + + @Test + void decoderDynamicHeaderTableMaxSizeLimitedByConfig() throws Exception { + final InboundDynamicTable dynamicTable = new InboundDynamicTable(4096); + final HPackDecoder decoder = new HPackDecoder(dynamicTable, StandardCharsets.UTF_8); + //do not increase max size, this should limit requests from the receiver to increase max size + + //emulate receiving header that illegally increases the table size above our max + final ByteArrayBuffer buf = new ByteArrayBuffer(128); + HPackEncoder.encodeInt(buf, 5, 8192, 0x20); + Assertions.assertThrows(HPackException.class, () -> decoder.decodeHeaders(wrap(buf))); + } + } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2RequestConverter.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2RequestConverter.java index f985dcf187..3c1a03fb73 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2RequestConverter.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2RequestConverter.java @@ -80,20 +80,6 @@ void testConvertFromFieldsUpperCaseHeaderName() { "Header name ':Path' is invalid (header name contains uppercase characters)"); } - @Test - void testConvertFromFieldsConnectionHeader() { - final List

headers = Arrays.asList( - new BasicHeader(":method", "GET"), - new BasicHeader(":scheme", "http"), - new BasicHeader(":authority", "www.example.com"), - new BasicHeader(":path", "/"), - new BasicHeader("connection", "keep-alive")); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(headers), - "Header 'connection: keep-alive' is illegal for HTTP/2 messages"); - } - @Test void testConvertFromFieldsPseudoHeaderSequence() { final List
headers = Arrays.asList( @@ -346,76 +332,6 @@ void testConvertFromMessageConnectWithPath() { "CONNECT request path must be null"); } - @Test - void testConvertFromMessageConnectionHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("Connection", "Keep-Alive"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'Connection: Keep-Alive' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsKeepAliveHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("Keep-Alive", "timeout=5, max=1000"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'Keep-Alive: timeout=5, max=1000' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsProxyConnectionHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("Proxy-Connection", "keep-alive"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'Proxy-Connection: Keep-Alive' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsTransferEncodingHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("Transfer-Encoding", "gzip"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'Transfer-Encoding: gzip' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsHostHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("Host", "host"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'Host: host' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsUpgradeHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("Upgrade", "example/1, foo/2"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'Upgrade: example/1, foo/2' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsTEHeader() { - final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); - request.addHeader("TE", "gzip"); - - final DefaultH2RequestConverter converter = new DefaultH2RequestConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(request), - "Header 'TE: gzip' is illegal for HTTP/2 messages"); - } - @Test void testConvertFromFieldsTETrailerHeader() throws Exception { diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2ResponseConverter.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2ResponseConverter.java index 2be82e2c5a..b5abb6aa87 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2ResponseConverter.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestDefaultH2ResponseConverter.java @@ -83,54 +83,6 @@ void testConvertFromFieldsInvalidStatusCode() { Assertions.assertThrows(HttpException.class, () -> converter.convert(headers)); } - @Test - void testConvertFromFieldsConnectionHeader() { - final List
headers = Arrays.asList( - new BasicHeader(":status", "200"), - new BasicHeader("location", "http://www.example.com/"), - new BasicHeader("connection", "keep-alive")); - - final DefaultH2ResponseConverter converter = new DefaultH2ResponseConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(headers), - "Header 'connection: keep-alive' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsKeepAliveHeader() { - final List
headers = Arrays.asList( - new BasicHeader(":status", "200"), - new BasicHeader("location", "http://www.example.com/"), - new BasicHeader("keep-alive", "timeout=5, max=1000")); - - final DefaultH2ResponseConverter converter = new DefaultH2ResponseConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(headers), - "Header 'keep-alive: timeout=5, max=1000' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsTransferEncodingHeader() { - final List
headers = Arrays.asList( - new BasicHeader(":status", "200"), - new BasicHeader("location", "http://www.example.com/"), - new BasicHeader("transfer-encoding", "gzip")); - - final DefaultH2ResponseConverter converter = new DefaultH2ResponseConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(headers), - "Header 'transfer-encoding: gzip' is illegal for HTTP/2 messages"); - } - - @Test - void testConvertFromFieldsUpgradeHeader() { - final List
headers = Arrays.asList( - new BasicHeader(":status", "200"), - new BasicHeader("location", "http://www.example.com/"), - new BasicHeader("upgrade", "example/1, foo/2")); - - final DefaultH2ResponseConverter converter = new DefaultH2ResponseConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(headers), - "Header 'upgrade: example/1, foo/2' is illegal for HTTP/2 messages"); - } - @Test void testConvertFromFieldsMissingStatus() { final List
headers = Arrays.asList( @@ -198,23 +150,34 @@ void testConvertFromMessageInvalidStatus() { } @Test - void testConvertFromMessageConnectionHeader() { + void testConvertFromMessageInvalidHeader() { final HttpResponse response = new BasicHttpResponse(200); - response.addHeader("Connection", "Keep-Alive"); + response.addHeader(":custom", "stuff"); final DefaultH2ResponseConverter converter = new DefaultH2ResponseConverter(); Assertions.assertThrows(HttpException.class, () -> converter.convert(response), - "Header 'Connection: Keep-Alive' is illegal for HTTP/2 messages"); + "Header name ':custom' is invalid"); } @Test - void testConvertFromMessageInvalidHeader() { - final HttpResponse response = new BasicHttpResponse(200); - response.addHeader(":custom", "stuff"); + void testConvertFromFieldsMultipleCookies() throws Exception { + final List
headers = Arrays.asList( + new BasicHeader(":status", "200"), + new BasicHeader("cookie", "a=b"), + new BasicHeader("location", "http://www.example.com/"), + new BasicHeader("cookie", "c=d"), + new BasicHeader("cookie", "e=f")); final DefaultH2ResponseConverter converter = new DefaultH2ResponseConverter(); - Assertions.assertThrows(HttpException.class, () -> converter.convert(response), - "Header name ':custom' is invalid"); + final HttpResponse response = converter.convert(headers); + Assertions.assertNotNull(response ); + Assertions.assertEquals(200, response .getCode()); + final Header[] allHeaders = response.getHeaders(); + Assertions.assertEquals(2, allHeaders.length); + Assertions.assertEquals("location", allHeaders[0].getName()); + Assertions.assertEquals("http://www.example.com/", allHeaders[0].getValue()); + Assertions.assertEquals("cookie", allHeaders[1].getName()); + Assertions.assertEquals("a=b; c=d; e=f", allHeaders[1].getValue()); } } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestFieldValidationSupport.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestFieldValidationSupport.java new file mode 100644 index 0000000000..e4f455809b --- /dev/null +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/TestFieldValidationSupport.java @@ -0,0 +1,99 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http2.impl; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TestFieldValidationSupport { + + @Test + void testNameCharValid() throws Exception { + Assertions.assertFalse(FieldValidationSupport.isNameCharValid(' ')); + Assertions.assertTrue(FieldValidationSupport.isNameCharValid('a')); + Assertions.assertTrue(FieldValidationSupport.isNameCharValid('A')); + Assertions.assertTrue(FieldValidationSupport.isNameCharValid('0')); + Assertions.assertTrue(FieldValidationSupport.isNameCharValid('@')); + Assertions.assertFalse(FieldValidationSupport.isNameCharValid(':')); + Assertions.assertFalse(FieldValidationSupport.isNameCharValid('ä')); + } + + @Test + void testNameCharLowerCaseValid() throws Exception { + Assertions.assertFalse(FieldValidationSupport.isNameCharLowerCaseValid(' ')); + Assertions.assertTrue(FieldValidationSupport.isNameCharLowerCaseValid('a')); + Assertions.assertFalse(FieldValidationSupport.isNameCharLowerCaseValid('A')); + Assertions.assertTrue(FieldValidationSupport.isNameCharLowerCaseValid('0')); + Assertions.assertTrue(FieldValidationSupport.isNameCharLowerCaseValid('@')); + Assertions.assertFalse(FieldValidationSupport.isNameCharLowerCaseValid(':')); + Assertions.assertFalse(FieldValidationSupport.isNameCharLowerCaseValid('ä')); + } + + @Test + void testNameValid() throws Exception { + Assertions.assertTrue(FieldValidationSupport.isNameValid("ABCDEF0123456789")); + Assertions.assertFalse(FieldValidationSupport.isNameValid(":Blah")); + Assertions.assertFalse(FieldValidationSupport.isNameValid("Blah ")); + Assertions.assertFalse(FieldValidationSupport.isNameValid("Bläh")); + } + + @Test + void testNameLowerCaseValid() throws Exception { + Assertions.assertTrue(FieldValidationSupport.isNameValid("abcdef0123456789")); + Assertions.assertFalse(FieldValidationSupport.isNameValid(":blah")); + Assertions.assertFalse(FieldValidationSupport.isNameValid("blah ")); + Assertions.assertFalse(FieldValidationSupport.isNameValid("bläh")); + } + + @Test + void testValueCharValid() throws Exception { + Assertions.assertTrue(FieldValidationSupport.isValueCharValid(' ')); + Assertions.assertTrue(FieldValidationSupport.isValueCharValid('a')); + Assertions.assertTrue(FieldValidationSupport.isValueCharValid('A')); + Assertions.assertTrue(FieldValidationSupport.isValueCharValid('0')); + Assertions.assertTrue(FieldValidationSupport.isValueCharValid('@')); + Assertions.assertTrue(FieldValidationSupport.isValueCharValid(':')); + Assertions.assertTrue(FieldValidationSupport.isValueCharValid('ä')); + Assertions.assertFalse(FieldValidationSupport.isValueCharValid((char) 0x00)); + Assertions.assertFalse(FieldValidationSupport.isValueCharValid('\n')); + Assertions.assertFalse(FieldValidationSupport.isValueCharValid('\r')); + } + + @Test + void testValueValid() throws Exception { + Assertions.assertTrue(FieldValidationSupport.isValueValid("ABCDEF0123456789")); + Assertions.assertTrue(FieldValidationSupport.isValueValid(":Blah")); + Assertions.assertTrue(FieldValidationSupport.isValueValid("Bläh")); + Assertions.assertFalse(FieldValidationSupport.isValueValid(" Blah")); + Assertions.assertFalse(FieldValidationSupport.isValueValid("Blah\t")); + Assertions.assertFalse(FieldValidationSupport.isValueValid("Blah\nBlah")); + Assertions.assertFalse(FieldValidationSupport.isValueValid("\rBlah")); + } + +} + diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/io/MultiByteArrayInputStream.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/io/MultiByteArrayInputStream.java index d4d0768f94..0d6fc6dd19 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/io/MultiByteArrayInputStream.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/io/MultiByteArrayInputStream.java @@ -73,8 +73,8 @@ public int read() throws IOException { public int read(final byte b[], final int off, final int len) throws IOException { if (b == null) { throw new NullPointerException(); - } else if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) > b.length) || ((off + len) < 0)) { + } else if (off < 0 || off > b.length || len < 0 || + (off + len) > b.length || (off + len) < 0) { throw new IndexOutOfBoundsException(); } if (len == 0) { diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java index 976d1913bb..61afaf8553 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java @@ -27,49 +27,78 @@ package org.apache.hc.core5.http2.impl.nio; -import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.locks.Lock; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.config.CharCodingConfig; -import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; +import org.apache.hc.core5.http.impl.CharCodingSupport; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.AsyncPushProducer; import org.apache.hc.core5.http.nio.HandlerFactory; -import org.apache.hc.core5.http.nio.command.ExecutableCommand; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http2.H2ConnectionException; +import org.apache.hc.core5.http2.H2Error; +import org.apache.hc.core5.http2.H2StreamResetException; import org.apache.hc.core5.http2.WritableByteChannelMock; import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.config.H2Param; +import org.apache.hc.core5.http2.config.H2Setting; import org.apache.hc.core5.http2.frame.DefaultFrameFactory; import org.apache.hc.core5.http2.frame.FrameConsts; import org.apache.hc.core5.http2.frame.FrameFactory; import org.apache.hc.core5.http2.frame.FrameType; import org.apache.hc.core5.http2.frame.RawFrame; import org.apache.hc.core5.http2.frame.StreamIdGenerator; +import org.apache.hc.core5.http2.hpack.HPackEncoder; import org.apache.hc.core5.reactor.ProtocolIOSession; +import org.apache.hc.core5.util.ByteArrayBuffer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; class TestAbstractH2StreamMultiplexer { + private static final FrameFactory FRAME_FACTORY = DefaultFrameFactory.INSTANCE; + @Mock ProtocolIOSession protocolIOSession; @Mock + Lock lock; + @Mock HttpProcessor httpProcessor; @Mock H2StreamListener h2StreamListener; + @Mock + H2StreamHandler streamHandler; + @Captor + ArgumentCaptor> headersCaptor; + @Captor + ArgumentCaptor exceptionCaptor; @BeforeEach void prepareMocks() { MockitoAnnotations.openMocks(this); + Mockito.when(protocolIOSession.getLock()).thenReturn(lock); } static class H2StreamMultiplexerImpl extends AbstractH2StreamMultiplexer { + private Supplier streamHandlerSupplier; + public H2StreamMultiplexerImpl( final ProtocolIOSession ioSession, final FrameFactory frameFactory, @@ -77,8 +106,26 @@ public H2StreamMultiplexerImpl( final HttpProcessor httpProcessor, final CharCodingConfig charCodingConfig, final H2Config h2Config, - final H2StreamListener streamListener) { + final H2StreamListener streamListener, + final Supplier streamHandlerSupplier) { super(ioSession, frameFactory, idGenerator, httpProcessor, charCodingConfig, h2Config, streamListener); + this.streamHandlerSupplier = streamHandlerSupplier; + } + + @Override + void validateSetting(final H2Param param, final int value) throws H2ConnectionException { + } + + @Override + H2Setting[] generateSettings(final H2Config localConfig) { + return new H2Setting[] { + new H2Setting(H2Param.HEADER_TABLE_SIZE, localConfig.getHeaderTableSize()), + new H2Setting(H2Param.ENABLE_PUSH, localConfig.isPushEnabled() ? 1 : 0), + new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()), + new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()), + new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()), + new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()) + }; } @Override @@ -94,22 +141,35 @@ void acceptPushFrame() throws H2ConnectionException { } @Override - H2StreamHandler createRemotelyInitiatedStream( - final H2StreamChannel channel, - final HttpProcessor httpProcessor, - final BasicHttpConnectionMetrics connMetrics, - final HandlerFactory pushHandlerFactory) throws IOException { + H2Stream incomingRequest(final H2StreamChannel channel) { + return new H2Stream(channel, streamHandlerSupplier.get()); + } + + @Override + H2Stream outgoingRequest(final H2StreamChannel channel, + final AsyncClientExchangeHandler exchangeHandler, + final HandlerFactory pushHandlerFactory, + final HttpContext context) { return null; } @Override - H2StreamHandler createLocallyInitiatedStream( - final ExecutableCommand command, - final H2StreamChannel channel, - final HttpProcessor httpProcessor, - final BasicHttpConnectionMetrics connMetrics) throws IOException { + H2Stream incomingPushPromise(final H2StreamChannel channel, + final HandlerFactory pushHandlerFactory) { + return new H2Stream(channel, streamHandlerSupplier.get()); + } + + @Override + H2Stream outgoingPushPromise(final H2StreamChannel channel, + final AsyncPushProducer pushProducer) { return null; } + + @Override + boolean allowGracefulAbort(final H2Stream stream) { + return stream.isRemoteClosed() && !stream.isLocalClosed(); + } + } @Test @@ -128,14 +188,15 @@ void testInputOneFrame() throws Exception { final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( protocolIOSession, - DefaultFrameFactory.INSTANCE, + FRAME_FACTORY, StreamIdGenerator.ODD, httpProcessor, CharCodingConfig.DEFAULT, H2Config.custom() .setMaxFrameSize(FrameConsts.MIN_FRAME_SIZE) .build(), - h2StreamListener); + h2StreamListener, + () -> streamHandler); Assertions.assertThrows(H2ConnectionException.class, () -> streamMultiplexer.onInput(ByteBuffer.wrap(bytes))); @@ -179,14 +240,15 @@ void testInputMultipleFrames() throws Exception { final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( protocolIOSession, - DefaultFrameFactory.INSTANCE, + FRAME_FACTORY, StreamIdGenerator.ODD, httpProcessor, CharCodingConfig.DEFAULT, H2Config.custom() .setMaxFrameSize(FrameConsts.MIN_FRAME_SIZE) .build(), - h2StreamListener); + h2StreamListener, + () -> streamHandler); Assertions.assertThrows(H2ConnectionException.class, () -> streamMultiplexer.onInput(ByteBuffer.wrap(bytes))); @@ -212,5 +274,505 @@ void testInputMultipleFrames() throws Exception { }); } + @Test + void testInputHeaderContinuationFrame() throws Exception { + final H2Config h2Config = H2Config.custom().setMaxFrameSize(FrameConsts.MIN_FRAME_SIZE) + .build(); + + final ByteArrayBuffer buf = new ByteArrayBuffer(19); + final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + final List
headers = new ArrayList<>(); + headers.add(new BasicHeader("test-header-key", "value")); + headers.add(new BasicHeader(":status", "200")); + encoder.encodeHeaders(buf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(2, ByteBuffer.wrap(buf.array(), 0, 10), false, false); + outBuffer.write(headerFrame, writableChannel); + final RawFrame continuationFrame = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(buf.array(), 10, 9), true); + outBuffer.write(continuationFrame, writableChannel); + final byte[] bytes = writableChannel.toByteArray(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + streamMultiplexer.onInput(ByteBuffer.wrap(bytes)); + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(false)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + } + + @Test + void testZeroIncrement() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.EVEN, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(200); + final HPackEncoder encoder = new HPackEncoder(h2Config.getHeaderTableSize(), + CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + + final List
headers = Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "http"), + new BasicHeader(":path", "/"), + new BasicHeader(":authority", "www.example.com")); + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(true)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + + writableChannel.reset(); + final ByteBuffer payload = ByteBuffer.allocate(4); + payload.putInt(0); + payload.flip(); + final RawFrame incrementFrame = new RawFrame(FrameType.WINDOW_UPDATE.getValue(), 0, 1, payload); + outBuffer.write(incrementFrame, writableChannel); + + final H2ConnectionException exception = Assertions.assertThrows(H2ConnectionException.class, () -> + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()))); + Assertions.assertEquals(H2Error.PROTOCOL_ERROR, H2Error.getByCode(exception.getCode())); + } + + @Test + void testIncrementOverflow() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.EVEN, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(200); + final HPackEncoder encoder = new HPackEncoder(h2Config.getHeaderTableSize(), + CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + + final List
headers = Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "http"), + new BasicHeader(":path", "/"), + new BasicHeader(":authority", "www.example.com")); + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(true)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + + writableChannel.reset(); + final RawFrame incrementFrame1 = FRAME_FACTORY.createWindowUpdate(1, 100); + outBuffer.write(incrementFrame1, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame incrementFrame2 = FRAME_FACTORY.createWindowUpdate(1, 0x7fffffff - 50); + outBuffer.write(incrementFrame2, writableChannel); + final H2ConnectionException exception = Assertions.assertThrows(H2ConnectionException.class, () -> + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()))); + Assertions.assertEquals(H2Error.FLOW_CONTROL_ERROR, H2Error.getByCode(exception.getCode())); + } + + @Test + void testHeadersAfterEndOfStream() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.EVEN, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(200); + final HPackEncoder encoder = new HPackEncoder(h2Config.getHeaderTableSize(), + CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + + final List
headers = Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "http"), + new BasicHeader(":path", "/"), + new BasicHeader(":authority", "www.example.com")); + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame1 = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame1, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(true)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + + writableChannel.reset(); + final RawFrame headerFrame2 = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame2, writableChannel); + + // Treat the first occurrence as a stream error + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + Mockito.verify(streamHandler).failed(exceptionCaptor.capture()); + Assertions.assertInstanceOf(H2StreamResetException.class, exceptionCaptor.getValue()); + + writableChannel.reset(); + final RawFrame headerFrame3 = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame3, writableChannel); + + // Treat subsequent occurrences as a connection-wide protocol error + final H2ConnectionException exception = Assertions.assertThrows(H2ConnectionException.class, () -> + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()))); + Assertions.assertEquals(H2Error.STREAM_CLOSED, H2Error.getByCode(exception.getCode())); + } + + @Test + void testDataAfterEndOfStream() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.EVEN, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(200); + final HPackEncoder encoder = new HPackEncoder(h2Config.getHeaderTableSize(), + CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + + final List
headers = Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "http"), + new BasicHeader(":path", "/"), + new BasicHeader(":authority", "www.example.com")); + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame1 = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame1, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(true)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + + writableChannel.reset(); + final RawFrame dataFrame1 = FRAME_FACTORY.createData(1, ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}), true); + outBuffer.write(dataFrame1, writableChannel); + + // Treat the first occurrence as a stream error + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + Mockito.verify(streamHandler).failed(exceptionCaptor.capture()); + Assertions.assertInstanceOf(H2StreamResetException.class, exceptionCaptor.getValue()); + + writableChannel.reset(); + final RawFrame dataFrame2 = FRAME_FACTORY.createData(1, ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}), true); + outBuffer.write(dataFrame2, writableChannel); + + // Treat subsequent occurrences as a connection-wide protocol error + final H2ConnectionException exception = Assertions.assertThrows(H2ConnectionException.class, () -> + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()))); + Assertions.assertEquals(H2Error.STREAM_CLOSED, H2Error.getByCode(exception.getCode())); + } + + @Test + void testContinuationAfterEndOfStream() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.EVEN, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(200); + final HPackEncoder encoder = new HPackEncoder(h2Config.getHeaderTableSize(), + CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + + final List
headers = Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "http"), + new BasicHeader(":path", "/"), + new BasicHeader(":authority", "www.example.com")); + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame1 = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true, true); + outBuffer.write(headerFrame1, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(true)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + + writableChannel.reset(); + final RawFrame continuationFrame = FRAME_FACTORY.createContinuation(1, ByteBuffer.wrap(headerBuf.array(), 0, headerBuf.length()), true); + outBuffer.write(continuationFrame, writableChannel); + + final H2ConnectionException exception = Assertions.assertThrows(H2ConnectionException.class, () -> + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()))); + Assertions.assertEquals(H2Error.PROTOCOL_ERROR, H2Error.getByCode(exception.getCode())); + } + + + @Test + void testInputHeaderContinuationFramesNoLimit() throws Exception { + final H2Config h2Config = H2Config.custom() + .setMaxContinuations(Integer.MAX_VALUE) + .build(); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(19); + final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + final List
headers = new ArrayList<>(); + headers.add(new BasicHeader(":status", "200")); + for (int i = 1; i <= 100; i++) { + headers.add(new BasicHeader("test-header-key-" + i, "value-" + i)); + } + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + Assertions.assertTrue(headerBuf.length() > 750); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(2, ByteBuffer.wrap(headerBuf.array(), 0, 250), false, false); + outBuffer.write(headerFrame, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame continuationFrame1 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 250, 250), false); + outBuffer.write(continuationFrame1, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame continuationFrame2 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 500, 250), false); + outBuffer.write(continuationFrame2, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame continuationFrame3 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 750, headerBuf.length() - 750), true); + outBuffer.write(continuationFrame3, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(false)); + Assertions.assertFalse(headersCaptor.getValue().isEmpty()); + } + + @Test + void testInputHeaderContinuationFramesMaxLimit() throws Exception { + final H2Config h2Config = H2Config.custom() + .setMaxContinuations(2) + .build(); + + final ByteArrayBuffer headerBuf = new ByteArrayBuffer(19); + final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + final List
headers = new ArrayList<>(); + headers.add(new BasicHeader(":status", "200")); + for (int i = 1; i <= 100; i++) { + headers.add(new BasicHeader("test-header-key-" + i, "value-" + i)); + } + encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled()); + + Assertions.assertTrue(headerBuf.length() > 750); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(2, ByteBuffer.wrap(headerBuf.array(), 0, 250), false, false); + outBuffer.write(headerFrame, writableChannel); + + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame continuationFrame1 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 250, 250), false); + outBuffer.write(continuationFrame1, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame continuationFrame2 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 500, 250), false); + outBuffer.write(continuationFrame2, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame continuationFrame3 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 750, headerBuf.length() - 750), true); + outBuffer.write(continuationFrame3, writableChannel); + + Assertions.assertThrows(H2ConnectionException.class, () -> + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()))); + } + + @Test + void testStreamRemoteReset() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final H2StreamChannel channel = streamMultiplexer.createChannel(1); + final H2Stream stream = new H2Stream(channel, streamHandler); + streamMultiplexer.addStream(stream); + + final ByteArrayBuffer buf = new ByteArrayBuffer(19); + final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + final List
headers = new ArrayList<>(); + headers.add(new BasicHeader(":status", "200")); + encoder.encodeHeaders(buf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(buf.array(), 0, 10), true, false); + outBuffer.write(headerFrame, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Assertions.assertFalse(stream.isRemoteClosed()); + Assertions.assertFalse(stream.isLocalClosed()); + + final RawFrame resetFrame = FRAME_FACTORY.createResetStream(1, H2Error.NO_ERROR); + outBuffer.write(resetFrame, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Assertions.assertTrue(stream.isRemoteClosed()); + Assertions.assertTrue(stream.isLocalClosed()); + + final ArgumentCaptor exceptionCaptor = ArgumentCaptor.forClass(Exception.class); + Mockito.verify(streamHandler).failed(exceptionCaptor.capture()); + Assertions.assertInstanceOf(H2StreamResetException.class, exceptionCaptor.getValue()); + } + + @Test + void testStreamRemoteResetNoErrorRemoteAlreadyClosed() throws Exception { + final H2Config h2Config = H2Config.custom() + .build(); + + final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + h2Config, + h2StreamListener, + () -> streamHandler); + + final H2StreamChannel channel = streamMultiplexer.createChannel(1); + final H2Stream stream = new H2Stream(channel, streamHandler); + streamMultiplexer.addStream(stream); + + final ByteArrayBuffer buf = new ByteArrayBuffer(19); + final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT)); + final List
headers = new ArrayList<>(); + headers.add(new BasicHeader(":status", "200")); + encoder.encodeHeaders(buf, headers, h2Config.isCompressionEnabled()); + + final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024); + final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024); + + final RawFrame headerFrame = FRAME_FACTORY.createHeaders(1, ByteBuffer.wrap(buf.array(), 0, 10), true, false); + outBuffer.write(headerFrame, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + writableChannel.reset(); + final RawFrame dataFrame = FRAME_FACTORY.createData(1, ByteBuffer.wrap(new byte[] { 'D', 'o', 'n', 'e'}), true); + outBuffer.write(dataFrame, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Assertions.assertTrue(stream.isRemoteClosed()); + Assertions.assertFalse(stream.isLocalClosed()); + + writableChannel.reset(); + final RawFrame resetFrame = FRAME_FACTORY.createResetStream(1, H2Error.NO_ERROR); + outBuffer.write(resetFrame, writableChannel); + streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())); + + Assertions.assertTrue(stream.isRemoteClosed()); + Assertions.assertTrue(stream.isLocalClosed()); + + Mockito.verify(streamHandler, Mockito.never()).failed(ArgumentMatchers.any()); + } + } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameInOutBuffers.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameInOutBuffers.java index ce889f2e86..a415b60da5 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameInOutBuffers.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestFrameInOutBuffers.java @@ -29,7 +29,6 @@ import java.nio.ByteBuffer; -import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http2.H2ConnectionException; import org.apache.hc.core5.http2.H2CorruptFrameException; import org.apache.hc.core5.http2.ReadableByteChannelMock; @@ -255,8 +254,11 @@ void testReadFrameConnectionClosed() throws Exception { final ReadableByteChannelMock readableChannel = new ReadableByteChannelMock(new byte[] {}); Assertions.assertNull(inBuffer.read(readableChannel)); - Assertions.assertThrows(ConnectionClosedException.class, () -> - inBuffer.read(readableChannel)); + Assertions.assertFalse(inBuffer.isEndOfStream()); + Assertions.assertNull(inBuffer.read(readableChannel)); + Assertions.assertTrue(inBuffer.isEndOfStream()); + Assertions.assertNull(inBuffer.read(readableChannel)); + Assertions.assertTrue(inBuffer.isEndOfStream()); } @Test diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2Interceptors.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2Interceptors.java index 17ac94817e..38410c7871 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2Interceptors.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2Interceptors.java @@ -30,8 +30,13 @@ import java.nio.charset.StandardCharsets; import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ProtocolException; @@ -39,6 +44,8 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.message.BasicHttpResponse; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -141,4 +148,114 @@ void testH2RequestContentValidOptionsMethod() throws Exception { Assertions.assertNotNull(header1); } + @Test + void testH2RequestConformanceConnectionHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("Connection", "Keep-Alive"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(request, null, context), + "Header 'Connection: Keep-Alive' is illegal for HTTP/2 messages"); + } + + @Test + void testH2RequestConformanceKeepAliveHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("Keep-Alive", "timeout=5, max=1000"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(request, null, context), + "Header 'Keep-Alive: timeout=5, max=1000' is illegal for HTTP/2 messages"); + } + + @Test + void testH2RequestConformanceProxyConnectionHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("Proxy-Connection", "keep-alive"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(request, null, context), + "Header 'Proxy-Connection: Keep-Alive' is illegal for HTTP/2 messages"); + } + + @Test + void testH2RequestConformanceTransferEncodingHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("Transfer-Encoding", "gzip"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(request, null, context), + "Header 'Transfer-Encoding: gzip' is illegal for HTTP/2 messages"); + } + + @Test + void testH2RequestConformanceHostHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("Host", "host"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertDoesNotThrow(() -> interceptor.process(request, null, context), + "Header 'Host: host' is permissible for HTTP/2 messages"); + } + + @Test + void testH2RequestConformanceUpgradeHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("Upgrade", "example/1, foo/2"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(request, null, context), + "Header 'Upgrade: example/1, foo/2' is illegal for HTTP/2 messages"); + } + + @Test + void testH2RequestConformanceTEHeader() { + final HttpRequest request = new BasicHttpRequest("GET", new HttpHost("host"), "/"); + request.addHeader("TE", "gzip"); + + final H2RequestConformance interceptor = new H2RequestConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(request, null, context), + "Header 'TE: gzip' is illegal for HTTP/2 messages"); + } + + @Test + void testH2ResponseConformanceConnectionHeader() { + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); + response.addHeader("Connection", "Keep-Alive"); + + final H2ResponseConformance interceptor = new H2ResponseConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(response, null, context), + "Header 'connection: keep-alive' is illegal for HTTP/2 messages"); + } + + @Test + void testH2ResponseConformanceKeepAliveHeader() { + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); + response.addHeader("keep-alive", "timeout=5, max=1000"); + + final H2ResponseConformance interceptor = new H2ResponseConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(response, null, context), + "Header 'keep-alive: timeout=5, max=1000' is illegal for HTTP/2 messages"); + } + + @Test + void testH2ResponseConformanceTransferEncodingHeader() { + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); + response.addHeader("transfer-encoding", "gzip"); + + final H2ResponseConformance interceptor = new H2ResponseConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(response, null, context), + "Header 'transfer-encoding: gzip' is illegal for HTTP/2 messages"); + } + + @Test + void testH2ResponseConformanceUpgradeHeader() { + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); + response.addHeader("upgrade", "example/1, foo/2"); + + final H2ResponseConformance interceptor = new H2ResponseConformance(); + Assertions.assertThrows(HttpException.class, () -> interceptor.process(response, null, context), + "Header 'upgrade: example/1, foo/2' is illegal for HTTP/2 messages"); + } + } diff --git a/httpcore5-reactive/pom.xml b/httpcore5-reactive/pom.xml index 254101e1fa..ce78a19aea 100644 --- a/httpcore5-reactive/pom.xml +++ b/httpcore5-reactive/pom.xml @@ -27,7 +27,7 @@ httpcore5-parent org.apache.httpcomponents.core5 - 5.3.1-SNAPSHOT + 5.4-alpha1-SNAPSHOT 4.0.0 diff --git a/httpcore5-reactive/src/main/java/org/apache/hc/core5/reactive/ReactiveDataConsumer.java b/httpcore5-reactive/src/main/java/org/apache/hc/core5/reactive/ReactiveDataConsumer.java index 0414326043..0e85b24502 100644 --- a/httpcore5-reactive/src/main/java/org/apache/hc/core5/reactive/ReactiveDataConsumer.java +++ b/httpcore5-reactive/src/main/java/org/apache/hc/core5/reactive/ReactiveDataConsumer.java @@ -137,7 +137,7 @@ private void flushToSubscriber() { return; } ByteBuffer next; - while (requests.get() > 0 && ((next = buffers.poll()) != null)) { + while (requests.get() > 0 && (next = buffers.poll()) != null) { final int bytesFreed = next.remaining(); s.onNext(next); requests.decrementAndGet(); diff --git a/httpcore5-testing/docker/BUILDING.txt b/httpcore5-testing/docker/BUILDING.txt deleted file mode 100644 index ed734fa22b..0000000000 --- a/httpcore5-testing/docker/BUILDING.txt +++ /dev/null @@ -1,31 +0,0 @@ -Building Docker containers for compatibility tests -======================================================== - -Remark: omit sudo command if executing as root - -= Build Apache HTTPD image ---- -sudo docker build -t hc-tests-httpd apache-httpd ---- - -= Build Nginx image ---- -sudo docker build -t hc-tests-nginx nginx ---- - -= Build HTTPBIN image ---- -sudo docker build -t hc-tests-httpbin httpbin ---- - -= Start containers - ---- -sudo docker-compose up ---- - -= Execute H2 compatibility tests - ---- -H2CompatibilityTest http://localhost:8080 APACHE-HTTPD -H2CompatibilityTest http://localhost:8081 NGINX \ No newline at end of file diff --git a/httpcore5-testing/docker/apache-httpd/Dockerfile b/httpcore5-testing/docker/apache-httpd/Dockerfile deleted file mode 100644 index ac1d2cbdef..0000000000 --- a/httpcore5-testing/docker/apache-httpd/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM httpd:2.4 -MAINTAINER dev@hc.apache.org - -ENV var_dir /var/httpd -ENV www_dir ${var_dir}/www - -RUN apt-get update -RUN apt-get install -y subversion - -RUN mkdir -p ${var_dir} -RUN svn co --depth immediates http://svn.apache.org/repos/asf/httpcomponents/site ${www_dir} -RUN svn up --set-depth infinity ${www_dir}/images -RUN svn up --set-depth infinity ${www_dir}/css - -RUN sed -i -E 's/^\s*#\s*(LoadModule\s+http2_module\s+modules\/mod_http2.so)/\1/' conf/httpd.conf - -RUN sed -i -E 's/^\s*ServerAdmin.*$/ServerAdmin dev@hc.apache.org/' conf/httpd.conf -RUN sed -i -E 's/^\s*#\s*(Include\s+conf\/extra\/httpd-vhosts.conf)/\1/' conf/httpd.conf -COPY httpd-vhosts.conf /usr/local/apache2/conf/extra/httpd-vhosts.conf \ No newline at end of file diff --git a/httpcore5-testing/pom.xml b/httpcore5-testing/pom.xml index 03084bbb09..7741059258 100644 --- a/httpcore5-testing/pom.xml +++ b/httpcore5-testing/pom.xml @@ -28,7 +28,7 @@ org.apache.httpcomponents.core5 httpcore5-parent - 5.3.1-SNAPSHOT + 5.4-alpha1-SNAPSHOT httpcore5-testing Apache HttpComponents Core Integration Tests @@ -60,11 +60,6 @@ org.slf4j slf4j-api - - io.reactivex.rxjava2 - rxjava - ${rxjava.version} - io.reactivex.rxjava3 rxjava @@ -105,6 +100,16 @@ mockito-core test + + org.testcontainers + testcontainers + test + + + org.testcontainers + junit-jupiter + test + @@ -127,44 +132,6 @@ - - io.fabric8 - docker-maven-plugin - 0.45.0 - - - start-httpbin - pre-integration-test - - start - - - - - kennethreitz/httpbin - httpbin - - - MAGENTA - - - 8082:80 - - - - - - - - stop-test-servers - post-integration-test - - stop - - - - - @@ -189,4 +156,4 @@ - \ No newline at end of file + diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/benchmark/BenchmarkConfig.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/benchmark/BenchmarkConfig.java index 68a037b286..a769f077d2 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/benchmark/BenchmarkConfig.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/benchmark/BenchmarkConfig.java @@ -222,7 +222,7 @@ public String toString() { ", useAcceptGZip=" + useAcceptGZip + ", payloadText='" + payloadText + '\'' + ", soapAction='" + soapAction + '\'' + - ", forceHttp2=" + forceHttp2+ + ", forceHttp2=" + forceHttp2 + ", disableSSLVerification=" + disableSSLVerification + ", trustStorePath='" + trustStorePath + '\'' + ", identityStorePath='" + identityStorePath + '\'' + diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/SocksProxy.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/SocksProxy.java index f4a5609eb1..dc7629ef8a 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/SocksProxy.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/SocksProxy.java @@ -44,7 +44,10 @@ /** * Cheap and nasty SOCKS protocol version 5 proxy, recommended for use in unit tests only so we can test our SOCKS client code. + * + * @deprecated Do not use. */ +@Deprecated() public class SocksProxy { private static class SocksProxyHandler { @@ -76,13 +79,13 @@ public void run() { final Thread t2 = pumpStream(target.getInputStream(), output); try { t1.join(); - } catch (final InterruptedException e) { + } catch (final InterruptedException ignored) { } try { t2.join(); - } catch (final InterruptedException e) { + } catch (final InterruptedException ignored) { } - } catch (final IOException e) { + } catch (final IOException ignored) { } finally { parent.cleanupSocksProxyHandler(SocksProxyHandler.this); } @@ -178,7 +181,7 @@ private Thread pumpStream(final InputStream input, final OutputStream output) { output.write(buffer, 0, read); output.flush(); } - } catch (final IOException e) { + } catch (final IOException ignored) { } finally { shutdown(); } @@ -193,12 +196,12 @@ private Thread pumpStream(final InputStream input, final OutputStream output) { public void shutdown() { try { this.socket.close(); - } catch (final IOException e) { + } catch (final IOException ignored) { } if (this.remote != null) { try { this.remote.close(); - } catch (final IOException e) { + } catch (final IOException ignored) { } } } @@ -232,12 +235,12 @@ public void start() throws IOException { final Socket socket = server.accept(); startSocksProxyHandler(socket); } - } catch (final IOException e) { + } catch (final IOException ignored) { } finally { if (server != null) { try { server.close(); - } catch (final IOException e) { + } catch (final IOException ignored) { } server = null; } @@ -258,7 +261,7 @@ public void shutdown(final TimeValue timeout) throws InterruptedException { if (this.server != null) { try { this.server.close(); - } catch (final IOException e) { + } catch (final IOException ignored) { } finally { this.server = null; } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/classic/Wire.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/classic/Wire.java index 2a8cf7d03b..1b9f1a4d9f 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/classic/Wire.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/classic/Wire.java @@ -55,7 +55,7 @@ private void wire(final String header, final byte[] b, final int pos, final int buffer.insert(0, header); this.log.debug("{} {}", this.id, buffer); buffer.setLength(0); - } else if ((ch < Chars.SP) || (ch >= Chars.DEL)) { + } else if (ch < Chars.SP || ch >= Chars.DEL) { buffer.append("[0x"); buffer.append(Integer.toHexString(ch)); buffer.append("]"); diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/ClientTestingAdapter.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/ClientTestingAdapter.java index ee2282e783..3d3067165a 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/ClientTestingAdapter.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/ClientTestingAdapter.java @@ -110,14 +110,14 @@ public Map execute(final String defaultURI, final Map request) { - return (adapter == null) || adapter.checkRequestSupport(request) == null; + return adapter == null || adapter.checkRequestSupport(request) == null; } /** * See the documentation for the same method in {@link ClientPOJOAdapter}. */ public Map modifyRequest(final Map request) { - return (adapter == null) ? request : adapter.modifyRequest(request); + return adapter == null ? request : adapter.modifyRequest(request); } /** diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFramework.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFramework.java index 5b90134c95..8368156cdd 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFramework.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFramework.java @@ -342,7 +342,7 @@ private void assertStatusMatchesExpectation(final Object actualStatus, final Obj if (actualStatus == null) { throw new TestingFrameworkException("Returned status is null."); } - if ((expectedStatus != null) && (! actualStatus.equals(expectedStatus))) { + if (expectedStatus != null && !actualStatus.equals(expectedStatus)) { throw new TestingFrameworkException("Expected status not found. expected=" + expectedStatus + "; actual=" + actualStatus); } @@ -353,7 +353,7 @@ private void assertBodyMatchesExpectation(final Object actualBody, final Object if (actualBody == null) { throw new TestingFrameworkException("Returned body is null."); } - if ((expectedBody != null) && (! actualBody.equals(expectedBody))) { + if (expectedBody != null && !actualBody.equals(expectedBody)) { throw new TestingFrameworkException("Expected body not found. expected=" + expectedBody + "; actual=" + actualBody); } @@ -373,7 +373,7 @@ private void assertContentTypeMatchesExpectation(final Object actualContentType, } private void assertHeadersMatchExpectation(final Map actualHeaders, - final Map expectedHeaders) + final Map expectedHeaders) throws TestingFrameworkException { if (expectedHeaders == null) { return; @@ -392,7 +392,7 @@ private void assertHeadersMatchExpectation(final Map actualHeade } private String getDefaultURI() { - return "http://localhost:" + port + "/"; + return "http://localhost:" + port + "/"; } /** diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java index e757b6232e..3ad934b09b 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java @@ -93,9 +93,9 @@ public void setDesiredResponse(final Map desiredResponse) throws */ public void assertNothingThrown() throws TestingFrameworkException { if (thrown != null) { - final TestingFrameworkException e = (thrown instanceof TestingFrameworkException ? + final TestingFrameworkException e = thrown instanceof TestingFrameworkException ? (TestingFrameworkException) thrown : - new TestingFrameworkException(thrown)); + new TestingFrameworkException(thrown); thrown = null; throw e; } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncRequester.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncRequester.java index 3c229034df..25e65e0f95 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncRequester.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncRequester.java @@ -65,7 +65,9 @@ DefaultConnectingIOReactor createIOReactor( LoggingIOSessionDecorator.INSTANCE, LoggingExceptionCallback.INSTANCE, LoggingIOSessionListener.INSTANCE, - sessionShutdownCallback); + LoggingReactorMetricsListener.INSTANCE, + sessionShutdownCallback, + null); } private InetSocketAddress toSocketAddress(final HttpHost host) { diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncServer.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncServer.java index 562d33e8f2..b334827041 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncServer.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/AsyncServer.java @@ -60,7 +60,9 @@ DefaultListeningIOReactor createIOReactor( LoggingIOSessionDecorator.INSTANCE, LoggingExceptionCallback.INSTANCE, LoggingIOSessionListener.INSTANCE, - sessionShutdownCallback); + LoggingReactorMetricsListener.INSTANCE, + sessionShutdownCallback, + null); } public Future listen(final InetSocketAddress address) { diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/ClientSessionEndpoint.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/ClientSessionEndpoint.java index ba3761b66e..f81e5354d7 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/ClientSessionEndpoint.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/ClientSessionEndpoint.java @@ -34,8 +34,8 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.concurrent.BasicFuture; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncPushConsumer; @@ -103,14 +103,7 @@ public Future execute( Asserts.check(!closed.get(), "Connection is already closed"); final BasicFuture future = new BasicFuture<>(callback); execute(new BasicClientExchangeHandler<>(requestProducer, responseConsumer, - new FutureContribution(future) { - - @Override - public void completed(final T result) { - future.completed(result); - } - - }), + new CompletingFutureContribution(future)), pushHandlerFactory, context); return future; } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestClient.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestClient.java index a8839f8380..5ba6845607 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestClient.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestClient.java @@ -30,15 +30,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Future; import javax.net.ssl.SSLContext; -import org.apache.hc.core5.concurrent.BasicFuture; -import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.function.Supplier; -import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.config.CharCodingConfig; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.impl.HttpProcessors; @@ -52,34 +47,23 @@ import org.apache.hc.core5.http2.nio.support.DefaultAsyncPushConsumerFactory; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.reactor.IOReactorStatus; -import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; import org.apache.hc.core5.util.Args; -import org.apache.hc.core5.util.Asserts; -import org.apache.hc.core5.util.Timeout; -public class H2TestClient extends AsyncRequester { +public class H2TestClient extends HttpTestClient { - private final SSLContext sslContext; - private final SSLSessionInitializer sslSessionInitializer; - private final SSLSessionVerifier sslSessionVerifier; private final List>> routeEntries; private H2Config h2Config; private Http1Config http1Config; - private HttpProcessor httpProcessor; public H2TestClient( final IOReactorConfig ioReactorConfig, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, final SSLSessionVerifier sslSessionVerifier) throws IOException { - super(ioReactorConfig); - this.sslContext = sslContext; - this.sslSessionInitializer = sslSessionInitializer; - this.sslSessionVerifier = sslSessionVerifier; + super(ioReactorConfig, sslContext, sslSessionInitializer, sslSessionVerifier); this.routeEntries = new ArrayList<>(); } @@ -87,10 +71,6 @@ public H2TestClient() throws IOException { this(IOReactorConfig.DEFAULT, null, null, null); } - private void ensureNotRunning() { - Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Client is already running"); - } - public void register(final String uriPattern, final Supplier supplier) { Args.notNull(uriPattern, "URI pattern"); Args.notNull(supplier, "Push consumer supplier"); @@ -116,13 +96,9 @@ public void configure(final Http1Config http1Config) { } /** - * @since 5.3 + * @deprecated Use {@link #startExecution(IOEventHandlerFactory)}. */ - public void configure(final HttpProcessor httpProcessor) { - ensureNotRunning(); - this.httpProcessor = httpProcessor; - } - + @Deprecated public void start(final IOEventHandlerFactory handlerFactory) throws IOException { super.execute(handlerFactory); } @@ -175,6 +151,7 @@ public void start(final Http1Config http1Config) throws IOException { start(null, http1Config); } + @Override public void start() throws Exception { if (http1Config != null) { start(new InternalClientProtocolNegotiationStarter( @@ -187,7 +164,8 @@ public void start() throws Exception { CharCodingConfig.DEFAULT, sslContext, sslSessionInitializer, - sslSessionVerifier)); + sslSessionVerifier, + LoggingExceptionCallback.INSTANCE)); } else { start(new InternalClientProtocolNegotiationStarter( httpProcessor != null ? httpProcessor : H2Processors.client(), @@ -199,33 +177,10 @@ public void start() throws Exception { CharCodingConfig.DEFAULT, sslContext, sslSessionInitializer, - sslSessionVerifier)); + sslSessionVerifier, + LoggingExceptionCallback.INSTANCE)); } } - public Future connect( - final HttpHost host, - final Timeout timeout, - final FutureCallback callback) { - final BasicFuture future = new BasicFuture<>(callback); - requestSession(host, timeout, new FutureContribution(future) { - - @Override - public void completed(final IOSession session) { - future.completed(new ClientSessionEndpoint(session)); - } - - }); - return future; - } - - public Future connect(final HttpHost host,final Timeout timeout) { - return connect(host, timeout, null); - } - - public Future connect(final String hostname, final int port, final Timeout timeout) { - return connect(new HttpHost(hostname, port), timeout, null); - } - } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestServer.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestServer.java index 237ef5b048..1d9629019d 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestServer.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/H2TestServer.java @@ -29,22 +29,16 @@ import java.io.IOException; import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Future; import javax.net.ssl.SSLContext; import org.apache.hc.core5.function.Decorator; -import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.config.CharCodingConfig; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.impl.HttpProcessors; import org.apache.hc.core5.http.impl.routing.RequestRouter; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; import org.apache.hc.core5.http.nio.support.BasicAsyncServerExpectationDecorator; -import org.apache.hc.core5.http.nio.support.BasicServerExchangeHandler; import org.apache.hc.core5.http.nio.support.DefaultAsyncResponseExchangeHandlerFactory; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http.protocol.UriPatternType; @@ -53,59 +47,26 @@ import org.apache.hc.core5.http2.impl.H2Processors; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.reactor.IOReactorStatus; -import org.apache.hc.core5.reactor.ListenerEndpoint; import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; -import org.apache.hc.core5.util.Args; -import org.apache.hc.core5.util.Asserts; -public class H2TestServer extends AsyncServer { - - private final SSLContext sslContext; - private final SSLSessionInitializer sslSessionInitializer; - private final SSLSessionVerifier sslSessionVerifier; - private final List>> routeEntries; +public class H2TestServer extends HttpTestServer { private H2Config h2Config; private Http1Config http1Config; - private HttpProcessor httpProcessor; - private Decorator exchangeHandlerDecorator; public H2TestServer( final IOReactorConfig ioReactorConfig, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, final SSLSessionVerifier sslSessionVerifier) throws IOException { - super(ioReactorConfig); - this.sslContext = sslContext; - this.sslSessionInitializer = sslSessionInitializer; - this.sslSessionVerifier = sslSessionVerifier; - this.routeEntries = new ArrayList<>(); + super(ioReactorConfig, sslContext, sslSessionInitializer, sslSessionVerifier); } public H2TestServer() throws IOException { this(IOReactorConfig.DEFAULT, null, null, null); } - private void ensureNotRunning() { - Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Server is already running"); - } - - public void register(final String uriPattern, final Supplier supplier) { - Args.notNull(uriPattern, "URI pattern"); - Args.notNull(supplier, "Exchange handler supplier"); - Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Server has already been started"); - ensureNotRunning(); - routeEntries.add(new RequestRouter.Entry<>(uriPattern, supplier)); - } - - public void register( - final String uriPattern, - final AsyncServerRequestHandler requestHandler) { - register(uriPattern, () -> new BasicServerExchangeHandler<>(requestHandler)); - } - /** * @since 5.3 */ @@ -140,6 +101,10 @@ public void configure(final Decorator exchangeHandle this.exchangeHandlerDecorator = exchangeHandlerDecorator; } + /** + * @deprecated Use {@link #startExecution(IOEventHandlerFactory)}. + */ + @Deprecated public void start(final IOEventHandlerFactory handlerFactory) throws IOException { execute(handlerFactory); } @@ -202,7 +167,7 @@ public InetSocketAddress start(final Http1Config http1Config) throws Exception { public InetSocketAddress start() throws Exception { if (http1Config != null) { - start(new InternalServerProtocolNegotiationStarter( + return startExecution(new InternalServerProtocolNegotiationStarter( httpProcessor != null ? httpProcessor : HttpProcessors.server(), new DefaultAsyncResponseExchangeHandlerFactory( RequestRouter.create(RequestRouter.LOCAL_AUTHORITY, UriPatternType.URI_PATTERN, routeEntries, RequestRouter.LOCAL_AUTHORITY_RESOLVER, null), @@ -213,9 +178,10 @@ public InetSocketAddress start() throws Exception { CharCodingConfig.DEFAULT, sslContext, sslSessionInitializer, - sslSessionVerifier)); + sslSessionVerifier, + LoggingExceptionCallback.INSTANCE)); } else { - start(new InternalServerProtocolNegotiationStarter( + return startExecution(new InternalServerProtocolNegotiationStarter( httpProcessor != null ? httpProcessor : H2Processors.server(), new DefaultAsyncResponseExchangeHandlerFactory( RequestRouter.create(RequestRouter.LOCAL_AUTHORITY, UriPatternType.URI_PATTERN, routeEntries, RequestRouter.LOCAL_AUTHORITY_RESOLVER, null), @@ -226,11 +192,9 @@ public InetSocketAddress start() throws Exception { CharCodingConfig.DEFAULT, sslContext, sslSessionInitializer, - sslSessionVerifier)); + sslSessionVerifier, + LoggingExceptionCallback.INSTANCE)); } - final Future future = listen(new InetSocketAddress(0)); - final ListenerEndpoint listener = future.get(); - return (InetSocketAddress) listener.getAddress(); } } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestClient.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestClient.java index 16caea318b..cc70b7c168 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestClient.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestClient.java @@ -28,55 +28,40 @@ package org.apache.hc.core5.testing.nio; import java.io.IOException; -import java.util.concurrent.Future; import javax.net.ssl.SSLContext; -import org.apache.hc.core5.concurrent.BasicFuture; -import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; -import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.config.CharCodingConfig; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy; import org.apache.hc.core5.http.impl.HttpProcessors; +import org.apache.hc.core5.http.nio.NHttpMessageParserFactory; +import org.apache.hc.core5.http.nio.NHttpMessageWriterFactory; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.reactor.IOReactorStatus; -import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; -import org.apache.hc.core5.util.Asserts; -import org.apache.hc.core5.util.Timeout; -public class Http1TestClient extends AsyncRequester { - - private final SSLContext sslContext; - private final SSLSessionInitializer sslSessionInitializer; - private final SSLSessionVerifier sslSessionVerifier; +public class Http1TestClient extends HttpTestClient { private Http1Config http1Config; - private HttpProcessor httpProcessor; + private NHttpMessageParserFactory responseParserFactory; + private NHttpMessageWriterFactory requestWriterFactory; public Http1TestClient( final IOReactorConfig ioReactorConfig, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, final SSLSessionVerifier sslSessionVerifier) throws IOException { - super(ioReactorConfig); - this.sslContext = sslContext; - this.sslSessionInitializer = sslSessionInitializer; - this.sslSessionVerifier = sslSessionVerifier; + super(ioReactorConfig, sslContext, sslSessionInitializer, sslSessionVerifier); } public Http1TestClient() throws IOException { this(IOReactorConfig.DEFAULT, null, null, null); } - private void ensureNotRunning() { - Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Client is already running"); - } - /** * @since 5.3 */ @@ -86,11 +71,19 @@ public void configure(final Http1Config http1Config) { } /** - * @since 5.3 + * @since 5.4 + */ + public void configure(final NHttpMessageParserFactory responseParserFactory) { + ensureNotRunning(); + this.responseParserFactory = responseParserFactory; + } + + /** + * @since 5.4 */ - public void configure(final HttpProcessor httpProcessor) { + public void configure(final NHttpMessageWriterFactory requestWriterFactory) { ensureNotRunning(); - this.httpProcessor = httpProcessor; + this.requestWriterFactory = requestWriterFactory; } /** @@ -113,39 +106,18 @@ public void start(final Http1Config http1Config) throws IOException { start(null, http1Config); } + @Override public void start() throws IOException { - execute(new InternalClientHttp1EventHandlerFactory( + startExecution(new InternalClientHttp1EventHandlerFactory( httpProcessor != null ? httpProcessor : HttpProcessors.client(), http1Config, CharCodingConfig.DEFAULT, DefaultConnectionReuseStrategy.INSTANCE, + responseParserFactory, + requestWriterFactory, sslContext, sslSessionInitializer, sslSessionVerifier)); } - public Future connect( - final HttpHost host, - final Timeout timeout, - final FutureCallback callback) { - final BasicFuture future = new BasicFuture<>(callback); - requestSession(host, timeout, new FutureContribution(future) { - - @Override - public void completed(final IOSession session) { - future.completed(new ClientSessionEndpoint(session)); - } - - }); - return future; - } - - public Future connect(final HttpHost host, final Timeout timeout) { - return connect(host, timeout, null); - } - - public Future connect(final String hostname, final int port, final Timeout timeout) { - return connect(new HttpHost(hostname, port), timeout, null); - } - } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestServer.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestServer.java index 341be62518..dcef8abb52 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestServer.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/Http1TestServer.java @@ -29,76 +29,47 @@ import java.io.IOException; import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Future; import javax.net.ssl.SSLContext; import org.apache.hc.core5.function.Decorator; -import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.config.CharCodingConfig; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy; import org.apache.hc.core5.http.impl.HttpProcessors; import org.apache.hc.core5.http.impl.routing.RequestRouter; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.NHttpMessageParserFactory; +import org.apache.hc.core5.http.nio.NHttpMessageWriterFactory; import org.apache.hc.core5.http.nio.support.BasicAsyncServerExpectationDecorator; -import org.apache.hc.core5.http.nio.support.BasicServerExchangeHandler; import org.apache.hc.core5.http.nio.support.DefaultAsyncResponseExchangeHandlerFactory; import org.apache.hc.core5.http.protocol.HttpProcessor; import org.apache.hc.core5.http.protocol.UriPatternType; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.reactor.IOReactorStatus; -import org.apache.hc.core5.reactor.ListenerEndpoint; import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; -import org.apache.hc.core5.util.Asserts; -public class Http1TestServer extends AsyncServer { - - private final List>> routeEntries; - private final SSLContext sslContext; - private final SSLSessionInitializer sslSessionInitializer; - private final SSLSessionVerifier sslSessionVerifier; +public class Http1TestServer extends HttpTestServer { private Http1Config http1Config; - private HttpProcessor httpProcessor; - private Decorator exchangeHandlerDecorator; + private NHttpMessageParserFactory requestParserFactory; + private NHttpMessageWriterFactory responseWriterFactory; public Http1TestServer( final IOReactorConfig ioReactorConfig, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, final SSLSessionVerifier sslSessionVerifier) throws IOException { - super(ioReactorConfig); - this.routeEntries = new ArrayList<>(); - this.sslContext = sslContext; - this.sslSessionInitializer = sslSessionInitializer; - this.sslSessionVerifier = sslSessionVerifier; + super(ioReactorConfig, sslContext, sslSessionInitializer, sslSessionVerifier); } public Http1TestServer() throws IOException { this(IOReactorConfig.DEFAULT, null, null, null); } - private void ensureNotRunning() { - Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Server is already running"); - } - - public void register(final String uriPattern, final Supplier supplier) { - ensureNotRunning(); - routeEntries.add(new RequestRouter.Entry<>(uriPattern, supplier)); - } - - public void register( - final String uriPattern, - final AsyncServerRequestHandler requestHandler) { - register(uriPattern, () -> new BasicServerExchangeHandler<>(requestHandler)); - } - /** * @since 5.3 */ @@ -108,26 +79,27 @@ public void configure(final Http1Config http1Config) { } /** - * @since 5.3 + * @since 5.4 */ - public void configure(final HttpProcessor httpProcessor) { + public void configure(final NHttpMessageParserFactory requestParserFactory) { ensureNotRunning(); - this.httpProcessor = httpProcessor; + this.requestParserFactory = requestParserFactory; } /** - * @since 5.3 + * @since 5.4 */ - public void configure(final Decorator exchangeHandlerDecorator) { + public void configure(final NHttpMessageWriterFactory responseWriterFactory) { ensureNotRunning(); - this.exchangeHandlerDecorator = exchangeHandlerDecorator; + this.responseWriterFactory = responseWriterFactory; } + /** + * @deprecated Use {@link #startExecution(IOEventHandlerFactory)}. + */ + @Deprecated public InetSocketAddress start(final IOEventHandlerFactory handlerFactory) throws Exception { - execute(handlerFactory); - final Future future = listen(new InetSocketAddress(0)); - final ListenerEndpoint listener = future.get(); - return (InetSocketAddress) listener.getAddress(); + return startExecution(handlerFactory); } /** @@ -154,8 +126,9 @@ public InetSocketAddress start(final HttpProcessor httpProcessor, final Http1Con return start(); } + @Override public InetSocketAddress start() throws Exception { - return start(new InternalServerHttp1EventHandlerFactory( + return startExecution(new InternalServerHttp1EventHandlerFactory( httpProcessor != null ? httpProcessor : HttpProcessors.server(), new DefaultAsyncResponseExchangeHandlerFactory( RequestRouter.create(RequestRouter.LOCAL_AUTHORITY, UriPatternType.URI_PATTERN, routeEntries, RequestRouter.LOCAL_AUTHORITY_RESOLVER, null), @@ -163,6 +136,8 @@ public InetSocketAddress start() throws Exception { http1Config, CharCodingConfig.DEFAULT, DefaultConnectionReuseStrategy.INSTANCE, + requestParserFactory, + responseWriterFactory, sslContext, sslSessionInitializer, sslSessionVerifier)); diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/HttpTestClient.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/HttpTestClient.java new file mode 100644 index 0000000000..c09f862920 --- /dev/null +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/HttpTestClient.java @@ -0,0 +1,106 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import java.io.IOException; +import java.util.concurrent.Future; + +import javax.net.ssl.SSLContext; + +import org.apache.hc.core5.concurrent.BasicFuture; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.apache.hc.core5.reactor.IOEventHandlerFactory; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorStatus; +import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; +import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; +import org.apache.hc.core5.util.Asserts; +import org.apache.hc.core5.util.Timeout; + +/** + * @since 5.4 + */ +public abstract class HttpTestClient extends AsyncRequester { + + final SSLContext sslContext; + final SSLSessionInitializer sslSessionInitializer; + final SSLSessionVerifier sslSessionVerifier; + + HttpProcessor httpProcessor; + + public HttpTestClient( + final IOReactorConfig ioReactorConfig, + final SSLContext sslContext, + final SSLSessionInitializer sslSessionInitializer, + final SSLSessionVerifier sslSessionVerifier) throws IOException { + super(ioReactorConfig); + this.sslContext = sslContext; + this.sslSessionInitializer = sslSessionInitializer; + this.sslSessionVerifier = sslSessionVerifier; + } + + public HttpTestClient() throws IOException { + this(IOReactorConfig.DEFAULT, null, null, null); + } + + public abstract void start() throws Exception; + + void ensureNotRunning() { + Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Client is already running"); + } + + public void configure(final HttpProcessor httpProcessor) { + ensureNotRunning(); + this.httpProcessor = httpProcessor; + } + + public Future connect( + final HttpHost host, + final Timeout timeout, + final FutureCallback callback) { + final BasicFuture future = new BasicFuture<>(callback); + requestSession(host, timeout, new CompletingFutureContribution<>(future, ClientSessionEndpoint::new)); + return future; + } + + public Future connect(final HttpHost host, final Timeout timeout) { + return connect(host, timeout, null); + } + + public Future connect(final String hostname, final int port, final Timeout timeout) { + return connect(new HttpHost(hostname, port), timeout, null); + } + + public void startExecution(final IOEventHandlerFactory handlerFactory) throws IOException { + super.execute(handlerFactory); + } + +} diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/HttpTestServer.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/HttpTestServer.java new file mode 100644 index 0000000000..ec127406c9 --- /dev/null +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/HttpTestServer.java @@ -0,0 +1,120 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +import javax.net.ssl.SSLContext; + +import org.apache.hc.core5.function.Decorator; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.support.BasicServerExchangeHandler; +import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.apache.hc.core5.reactor.IOEventHandlerFactory; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorStatus; +import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer; +import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; + +/** + * @since 5.4 + */ +public abstract class HttpTestServer extends AsyncServer { + + final SSLContext sslContext; + final SSLSessionInitializer sslSessionInitializer; + final SSLSessionVerifier sslSessionVerifier; + final List>> routeEntries; + + HttpProcessor httpProcessor; + Decorator exchangeHandlerDecorator; + + public HttpTestServer( + final IOReactorConfig ioReactorConfig, + final SSLContext sslContext, + final SSLSessionInitializer sslSessionInitializer, + final SSLSessionVerifier sslSessionVerifier) throws IOException { + super(ioReactorConfig); + this.sslContext = sslContext; + this.sslSessionInitializer = sslSessionInitializer; + this.sslSessionVerifier = sslSessionVerifier; + this.routeEntries = new ArrayList<>(); + } + + public HttpTestServer() throws IOException { + this(IOReactorConfig.DEFAULT, null, null, null); + } + + public abstract InetSocketAddress start() throws Exception; + + void ensureNotRunning() { + Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Server is already running"); + } + + public void register(final String uriPattern, final Supplier supplier) { + Args.notNull(uriPattern, "URI pattern"); + Args.notNull(supplier, "Exchange handler supplier"); + Asserts.check(getStatus() == IOReactorStatus.INACTIVE, "Server has already been started"); + ensureNotRunning(); + routeEntries.add(new RequestRouter.Entry<>(uriPattern, supplier)); + } + + public void register( + final String uriPattern, + final AsyncServerRequestHandler requestHandler) { + register(uriPattern, () -> new BasicServerExchangeHandler<>(requestHandler)); + } + + public void configure(final HttpProcessor httpProcessor) { + ensureNotRunning(); + this.httpProcessor = httpProcessor; + } + + public void configure(final Decorator exchangeHandlerDecorator) { + ensureNotRunning(); + this.exchangeHandlerDecorator = exchangeHandlerDecorator; + } + + public InetSocketAddress startExecution(final IOEventHandlerFactory handlerFactory) throws Exception { + execute(handlerFactory); + final Future future = listen(new InetSocketAddress(0)); + final ListenerEndpoint listener = future.get(); + return (InetSocketAddress) listener.getAddress(); + } + +} diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientHttp1EventHandlerFactory.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientHttp1EventHandlerFactory.java index e28ab6bf86..360c121264 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientHttp1EventHandlerFactory.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientHttp1EventHandlerFactory.java @@ -77,6 +77,8 @@ class InternalClientHttp1EventHandlerFactory implements IOEventHandlerFactory { final Http1Config http1Config, final CharCodingConfig charCodingConfig, final ConnectionReuseStrategy connectionReuseStrategy, + final NHttpMessageParserFactory responseParserFactory, + final NHttpMessageWriterFactory requestWriterFactory, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, final SSLSessionVerifier sslSessionVerifier) { @@ -86,8 +88,8 @@ class InternalClientHttp1EventHandlerFactory implements IOEventHandlerFactory { this.connectionReuseStrategy = connectionReuseStrategy != null ? connectionReuseStrategy : DefaultConnectionReuseStrategy.INSTANCE; this.sslContext = sslContext; - this.responseParserFactory = new DefaultHttpResponseParserFactory(this.http1Config); - this.requestWriterFactory = DefaultHttpRequestWriterFactory.INSTANCE; + this.responseParserFactory = responseParserFactory != null ? responseParserFactory : new DefaultHttpResponseParserFactory(this.http1Config); + this.requestWriterFactory = requestWriterFactory != null ? requestWriterFactory : DefaultHttpRequestWriterFactory.INSTANCE; this.sslSessionInitializer = sslSessionInitializer; this.sslSessionVerifier = sslSessionVerifier; } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientProtocolNegotiationStarter.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientProtocolNegotiationStarter.java index 2453214593..cc13b12103 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientProtocolNegotiationStarter.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalClientProtocolNegotiationStarter.java @@ -29,6 +29,7 @@ import javax.net.ssl.SSLContext; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.config.CharCodingConfig; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.impl.HttpProcessors; @@ -64,6 +65,7 @@ class InternalClientProtocolNegotiationStarter implements IOEventHandlerFactory private final SSLContext sslContext; private final SSLSessionInitializer sslSessionInitializer; private final SSLSessionVerifier sslSessionVerifier; + private final Callback exceptionCallback; InternalClientProtocolNegotiationStarter( final HttpProcessor httpProcessor, @@ -74,7 +76,8 @@ class InternalClientProtocolNegotiationStarter implements IOEventHandlerFactory final CharCodingConfig charCodingConfig, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, - final SSLSessionVerifier sslSessionVerifier) { + final SSLSessionVerifier sslSessionVerifier, + final Callback exceptionCallback) { this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); this.exchangeHandlerFactory = exchangeHandlerFactory; this.versionPolicy = versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE; @@ -84,6 +87,7 @@ class InternalClientProtocolNegotiationStarter implements IOEventHandlerFactory this.sslContext = sslContext; this.sslSessionInitializer = sslSessionInitializer; this.sslSessionVerifier = sslSessionVerifier; + this.exceptionCallback = exceptionCallback; } @Override @@ -103,11 +107,11 @@ public IOEventHandler createHandler(final ProtocolIOSession ioSession, final Obj charCodingConfig, LoggingH2StreamListener.INSTANCE); ioSession.registerProtocol(ApplicationProtocol.HTTP_1_1.id, new ClientHttp1UpgradeHandler(http1StreamHandlerFactory)); - ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ClientH2UpgradeHandler(http2StreamHandlerFactory)); + ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ClientH2UpgradeHandler(http2StreamHandlerFactory, exceptionCallback)); switch (versionPolicy) { case FORCE_HTTP_2: - return new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, false); + return new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, false, exceptionCallback); case FORCE_HTTP_1: return new ClientHttp1IOEventHandler(http1StreamHandlerFactory.create(ioSession)); default: diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerHttp1EventHandlerFactory.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerHttp1EventHandlerFactory.java index c7487ec62d..2c81f580d1 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerHttp1EventHandlerFactory.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerHttp1EventHandlerFactory.java @@ -31,6 +31,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.ContentLengthStrategy; import org.apache.hc.core5.http.HttpRequest; @@ -82,6 +83,8 @@ class InternalServerHttp1EventHandlerFactory implements IOEventHandlerFactory { final Http1Config http1Config, final CharCodingConfig charCodingConfig, final ConnectionReuseStrategy connectionReuseStrategy, + final NHttpMessageParserFactory requestParserFactory, + final NHttpMessageWriterFactory responseWriterFactory, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, final SSLSessionVerifier sslSessionVerifier) { @@ -94,8 +97,8 @@ class InternalServerHttp1EventHandlerFactory implements IOEventHandlerFactory { this.sslContext = sslContext; this.sslSessionInitializer = sslSessionInitializer; this.sslSessionVerifier = sslSessionVerifier; - this.requestParserFactory = new DefaultHttpRequestParserFactory(this.http1Config); - this.responseWriterFactory = new DefaultHttpResponseWriterFactory(this.http1Config); + this.requestParserFactory = requestParserFactory != null ? requestParserFactory : new DefaultHttpRequestParserFactory(this.http1Config); + this.responseWriterFactory = responseWriterFactory != null ? responseWriterFactory : new DefaultHttpResponseWriterFactory(this.http1Config); } protected ServerHttp1StreamDuplexer createServerHttp1StreamDuplexer( @@ -109,11 +112,12 @@ protected ServerHttp1StreamDuplexer createServerHttp1StreamDuplexer( final NHttpMessageWriter outgoingMessageWriter, final ContentLengthStrategy incomingContentStrategy, final ContentLengthStrategy outgoingContentStrategy, - final Http1StreamListener streamListener) { + final Http1StreamListener streamListener, + final Callback exceptionCallback) { return new ServerHttp1StreamDuplexer(ioSession, httpProcessor, exchangeHandlerFactory, sslContext != null ? URIScheme.HTTPS.id : URIScheme.HTTP.id, http1Config, charCodingConfig, connectionReuseStrategy, incomingMessageParser, outgoingMessageWriter, - incomingContentStrategy, outgoingContentStrategy, streamListener); + incomingContentStrategy, outgoingContentStrategy, streamListener, exceptionCallback); } @Override @@ -132,7 +136,8 @@ public IOEventHandler createHandler(final ProtocolIOSession ioSession, final Obj responseWriterFactory.create(), DefaultContentLengthStrategy.INSTANCE, DefaultContentLengthStrategy.INSTANCE, - LoggingHttp1StreamListener.INSTANCE_SERVER); + LoggingHttp1StreamListener.INSTANCE_SERVER, + LoggingExceptionCallback.INSTANCE); return new ServerHttp1IOEventHandler(streamDuplexer); } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerProtocolNegotiationStarter.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerProtocolNegotiationStarter.java index dccf104601..cc953eba34 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerProtocolNegotiationStarter.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/InternalServerProtocolNegotiationStarter.java @@ -29,6 +29,7 @@ import javax.net.ssl.SSLContext; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.config.CharCodingConfig; import org.apache.hc.core5.http.config.Http1Config; @@ -65,6 +66,7 @@ class InternalServerProtocolNegotiationStarter implements IOEventHandlerFactory private final SSLContext sslContext; private final SSLSessionInitializer sslSessionInitializer; private final SSLSessionVerifier sslSessionVerifier; + private final Callback exceptionCallback; public InternalServerProtocolNegotiationStarter( final HttpProcessor httpProcessor, @@ -75,7 +77,8 @@ public InternalServerProtocolNegotiationStarter( final CharCodingConfig charCodingConfig, final SSLContext sslContext, final SSLSessionInitializer sslSessionInitializer, - final SSLSessionVerifier sslSessionVerifier) { + final SSLSessionVerifier sslSessionVerifier, + final Callback exceptionCallback) { this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); this.exchangeHandlerFactory = Args.notNull(exchangeHandlerFactory, "Exchange handler factory"); this.versionPolicy = versionPolicy != null ? versionPolicy : HttpVersionPolicy.NEGOTIATE; @@ -85,6 +88,7 @@ public InternalServerProtocolNegotiationStarter( this.sslContext = sslContext; this.sslSessionInitializer = sslSessionInitializer; this.sslSessionVerifier = sslSessionVerifier; + this.exceptionCallback = exceptionCallback; } @Override @@ -97,7 +101,8 @@ public IOEventHandler createHandler(final ProtocolIOSession ioSession, final Obj exchangeHandlerFactory, http1Config, charCodingConfig, - LoggingHttp1StreamListener.INSTANCE_SERVER); + LoggingHttp1StreamListener.INSTANCE_SERVER, + LoggingExceptionCallback.INSTANCE); final ServerH2StreamMultiplexerFactory http2StreamHandlerFactory = new ServerH2StreamMultiplexerFactory( httpProcessor != null ? httpProcessor : H2Processors.server(), exchangeHandlerFactory, @@ -105,11 +110,11 @@ public IOEventHandler createHandler(final ProtocolIOSession ioSession, final Obj charCodingConfig, LoggingH2StreamListener.INSTANCE); ioSession.registerProtocol(ApplicationProtocol.HTTP_1_1.id, new ServerHttp1UpgradeHandler(http1StreamHandlerFactory)); - ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ServerH2UpgradeHandler(http2StreamHandlerFactory)); + ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ServerH2UpgradeHandler(http2StreamHandlerFactory, exceptionCallback)); switch (versionPolicy) { case FORCE_HTTP_2: - return new ServerH2PrefaceHandler(ioSession, http2StreamHandlerFactory); + return new ServerH2PrefaceHandler(ioSession, http2StreamHandlerFactory, exceptionCallback); case FORCE_HTTP_1: return new ServerHttp1IOEventHandler(http1StreamHandlerFactory.create( sslContext != null ? URIScheme.HTTPS.id : URIScheme.HTTP.id, diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingHttp1StreamListener.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingHttp1StreamListener.java index 72796c2c77..48195baad6 100644 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingHttp1StreamListener.java +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingHttp1StreamListener.java @@ -34,11 +34,10 @@ import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.impl.Http1StreamListener; -import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.message.StatusLine; import org.apache.hc.core5.testing.classic.LoggingSupport; -import org.slf4j.LoggerFactory; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class LoggingHttp1StreamListener implements Http1StreamListener { @@ -61,7 +60,7 @@ private LoggingHttp1StreamListener(final Type type) { public void onRequestHead(final HttpConnection connection, final HttpRequest request) { if (headerLog.isDebugEnabled()) { final String idRequestDirection = LoggingSupport.getId(connection) + requestDirection; - headerLog.debug("{}{}", idRequestDirection, new RequestLine(request)); + headerLog.debug("{}{} {}", idRequestDirection, request.getMethod(), request.getRequestUri()); for (final Iterator
it = request.headerIterator(); it.hasNext(); ) { headerLog.debug("{}{}", idRequestDirection, it.next()); } diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingReactorMetricsListener.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingReactorMetricsListener.java new file mode 100644 index 0000000000..2202afe9a1 --- /dev/null +++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/nio/LoggingReactorMetricsListener.java @@ -0,0 +1,68 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import org.apache.hc.core5.reactor.IOReactorMetricsListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoggingReactorMetricsListener implements IOReactorMetricsListener { + + public static final IOReactorMetricsListener INSTANCE = new LoggingReactorMetricsListener(); + + private final Logger logger = LoggerFactory.getLogger("org.apache.hc.core5.http.pool"); + + @Override + public void onThreadPoolStatus(final int activeThreads, final int pendingConnections) { + if (logger.isDebugEnabled()) { + logger.debug("Active threads: {}, Pending connections: {}", activeThreads, pendingConnections); + } + } + + @Override + public void onThreadPoolSaturation(final double saturationPercentage) { + if (logger.isDebugEnabled()) { + logger.debug("Thread pool saturation: {}%", saturationPercentage); + } + } + + @Override + public void onResourceStarvationDetected() { + if (logger.isDebugEnabled()) { + logger.debug("Resource starvation detected!"); + } + } + + @Override + public void onQueueWaitTime(final long averageWaitTimeMillis) { + if (logger.isDebugEnabled()) { + logger.debug("Average queue wait time: {} ms", averageWaitTimeMillis); + } + } +} + diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/reactive/ReactiveTestUtils.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/reactive/ReactiveTestUtils.java deleted file mode 100644 index 1813cdd3a1..0000000000 --- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/reactive/ReactiveTestUtils.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.core5.testing.reactive; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Random; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.hc.core5.util.TextUtils; -import org.reactivestreams.Publisher; - -import io.reactivex.Emitter; -import io.reactivex.Flowable; -import io.reactivex.Single; -import io.reactivex.functions.Consumer; - -/** - * @deprecated Use {@link Reactive3TestUtils} and RxJava3 - */ -@Deprecated -public class ReactiveTestUtils { - /** The range from which to generate random data. */ - private final static byte[] RANGE = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - .getBytes(StandardCharsets.US_ASCII); - - /** - * Produces a deterministic stream of bytes, in randomly sized chunks of up to 128kB. - * - * @param length the number of bytes in the stream - * @return a reactive stream of bytes - */ - public static Flowable produceStream(final long length) { - return produceStream(length, null); - } - - /** - * Produces a deterministic stream of bytes, in randomly sized chunks of up to 128kB, while computing the hash of - * the random data. - * - * @param length the number of bytes in the stream - * @param hash an output argument for the hash, set when the end of the stream is reached; if {@code null}, the - * hash will not be computed - * @return a reactive stream of bytes - */ - public static Flowable produceStream(final long length, final AtomicReference hash) { - return produceStream(length, 128 * 1024, hash); - } - - /** - * Produces a deterministic stream of bytes, in randomly sized chunks, while computing the hash of the random data. - * - * @param length the number of bytes in the stream - * @param maximumBlockSize the maximum size of any {@code ByteBuffer in the stream} - * @param hash an output argument for the hash, set when the end of the stream is reached; if {@code null}, the - * hash will not be computed - * @return a reactive stream of bytes - */ - public static Flowable produceStream( - final long length, - final int maximumBlockSize, - final AtomicReference hash - ) { - return Flowable.generate(new Consumer>() { - final Random random = new Random(length); // Use the length as the random seed for easy reproducibility - long bytesEmitted; - final MessageDigest md = newMessageDigest(); - - @Override - public void accept(final Emitter emitter) { - final long remainingLength = length - bytesEmitted; - if (remainingLength == 0) { - emitter.onComplete(); - if (hash != null) { - hash.set(TextUtils.toHexString(md.digest())); - } - } else { - final int bufferLength = (int) Math.min(remainingLength, 1 + random.nextInt(maximumBlockSize)); - final byte[] bs = new byte[bufferLength]; - for (int i = 0; i < bufferLength; i++) { - final byte b = RANGE[(int) (random.nextDouble() * RANGE.length)]; - bs[i] = b; - } - if (hash != null) { - md.update(bs); - } - emitter.onNext(ByteBuffer.wrap(bs)); - bytesEmitted += bufferLength; - } - } - }); - } - - /** - * Computes the hash of the deterministic stream (as produced by {@link #produceStream(long)}). - */ - public static String getStreamHash(final long length) { - return TextUtils.toHexString(consumeStream(produceStream(length)).blockingGet().md.digest()); - } - - /** - * Consumes the given stream and returns a data structure containing its length and digest. - */ - public static Single consumeStream(final Publisher publisher) { - final StreamDescription seed = new StreamDescription(0, newMessageDigest()); - return Flowable.fromPublisher(publisher) - .reduce(seed, (desc, byteBuffer) -> { - final long length = desc.length + byteBuffer.remaining(); - desc.md.update(byteBuffer); - return new StreamDescription(length, desc.md); - }); - } - - private static MessageDigest newMessageDigest() { - try { - return MessageDigest.getInstance("MD5"); - } catch (final NoSuchAlgorithmException ex) { - throw new AssertionError(ex); - } - } - - public static class StreamDescription { - public final long length; - public final MessageDigest md; - - public StreamDescription(final long length, final MessageDigest md) { - this.length = length; - this.md = md; - } - } -} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/Result.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/Result.java new file mode 100644 index 0000000000..817bdc5572 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/Result.java @@ -0,0 +1,80 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.testing; + +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.message.RequestLine; +import org.apache.hc.core5.http.message.StatusLine; + +public final class Result { + + public final HttpRequest request; + public final HttpResponse response; + public final T content; + public final Exception exception; + + public enum Status { OK, NOK } + + public Result(final HttpRequest request, final Exception exception) { + this.request = request; + this.response = null; + this.content = null; + this.exception = exception; + } + + public Result(final HttpRequest request, final HttpResponse response, final T content) { + this.request = request; + this.response = response; + this.content = content; + this.exception = null; + } + + public Status getStatus() { + return exception != null ? Status.NOK : Status.OK; + } + + public boolean isOK() { + return exception == null; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append(new RequestLine(request)); + buf.append(" -> "); + if (exception != null) { + buf.append("NOK: ").append(exception); + } else { + if (response != null) { + buf.append("OK: ").append(new StatusLine(response)); + } + } + return buf.toString(); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/SSLTestContexts.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/SSLTestContexts.java index 52e1649775..785392785a 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/SSLTestContexts.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/SSLTestContexts.java @@ -27,31 +27,56 @@ package org.apache.hc.core5.testing; +import java.io.IOException; import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import javax.net.ssl.SSLContext; import org.apache.hc.core5.ssl.SSLContextBuilder; public final class SSLTestContexts { + public static SSLContext createServerSSLContext() { + return createServerSSLContext(null); + } - public static SSLContext createServerSSLContext() throws Exception { + public static SSLContext createServerSSLContext(final String protocol) { final URL keyStoreURL = SSLTestContexts.class.getResource("/test.p12"); final String storePassword = "nopassword"; - return SSLContextBuilder.create() - .setKeyStoreType("pkcs12") - .loadTrustMaterial(keyStoreURL, storePassword.toCharArray()) - .loadKeyMaterial(keyStoreURL, storePassword.toCharArray(), storePassword.toCharArray()) - .build(); + try { + return SSLContextBuilder.create() + .setKeyStoreType("pkcs12") + .loadTrustMaterial(keyStoreURL, storePassword.toCharArray()) + .loadKeyMaterial(keyStoreURL, storePassword.toCharArray(), storePassword.toCharArray()) + .setProtocol(protocol) + .build(); + } catch (final NoSuchAlgorithmException | KeyManagementException | KeyStoreException | CertificateException | + UnrecoverableKeyException | IOException ex) { + throw new IllegalStateException(ex); + } + } + + public static SSLContext createClientSSLContext() { + return createClientSSLContext(null); } - public static SSLContext createClientSSLContext() throws Exception { + public static SSLContext createClientSSLContext(final String protocol) { final URL keyStoreURL = SSLTestContexts.class.getResource("/test.p12"); final String storePassword = "nopassword"; - return SSLContextBuilder.create() - .setKeyStoreType("pkcs12") - .loadTrustMaterial(keyStoreURL, storePassword.toCharArray()) - .build(); + try { + return SSLContextBuilder.create() + .setKeyStoreType("pkcs12") + .loadTrustMaterial(keyStoreURL, storePassword.toCharArray()) + .setProtocol(protocol) + .build(); + } catch (final NoSuchAlgorithmException | KeyManagementException | KeyStoreException | CertificateException | + IOException ex) { + throw new IllegalStateException(ex); + } } } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicAuthenticationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicAuthenticationTest.java index ec0d142ba1..881508d98d 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicAuthenticationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicAuthenticationTest.java @@ -43,7 +43,6 @@ import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.Method; -import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.bootstrap.HttpRequester; import org.apache.hc.core5.http.impl.bootstrap.HttpServer; import org.apache.hc.core5.http.impl.bootstrap.StandardFilter; @@ -58,6 +57,7 @@ import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.net.URIAuthority; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; import org.apache.hc.core5.testing.extension.classic.HttpServerResource; import org.apache.hc.core5.util.Timeout; @@ -75,7 +75,8 @@ abstract class ClassicAuthenticationTest { private HttpRequesterResource clientResource; public ClassicAuthenticationTest(final Boolean respondImmediately) { - this.serverResource = new HttpServerResource(URIScheme.HTTP, bootstrap -> bootstrap + this.serverResource = new HttpServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setSocketConfig( SocketConfig.custom() .setSoTimeout(TIMEOUT) @@ -116,7 +117,9 @@ protected HttpEntity generateResponseContent(final HttpResponse unauthorized) { } }) ); - this.clientResource = new HttpRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setSslContext(SSLTestContexts.createClientSSLContext()) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1CoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1CoreTransportTest.java index a9e5b98577..8c1afe4b5a 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1CoreTransportTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1CoreTransportTest.java @@ -41,6 +41,7 @@ import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; import org.apache.hc.core5.testing.extension.classic.HttpServerResource; import org.apache.hc.core5.util.Timeout; @@ -57,7 +58,9 @@ abstract class ClassicHttp1CoreTransportTest extends ClassicHttpCoreTransportTes public ClassicHttp1CoreTransportTest(final URIScheme scheme) { super(scheme); - this.serverResource = new HttpServerResource(scheme, bootstrap -> bootstrap + this.serverResource = new HttpServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setSslContext(scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .build()) @@ -84,7 +87,9 @@ public void submitResponse( } }, context))); - this.clientResource = new HttpRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setSslContext(SSLTestContexts.createClientSSLContext()) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .build())); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1SocksProxyCoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1SocksProxyCoreTransportTest.java deleted file mode 100644 index deb3598ae5..0000000000 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicHttp1SocksProxyCoreTransportTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.core5.testing.classic; - -import java.io.IOException; - -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.HeaderElements; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.URIScheme; -import org.apache.hc.core5.http.impl.bootstrap.HttpRequester; -import org.apache.hc.core5.http.impl.bootstrap.HttpServer; -import org.apache.hc.core5.http.impl.bootstrap.StandardFilter; -import org.apache.hc.core5.http.impl.routing.RequestRouter; -import org.apache.hc.core5.http.io.HttpFilterChain; -import org.apache.hc.core5.http.io.HttpRequestHandler; -import org.apache.hc.core5.http.io.SocketConfig; -import org.apache.hc.core5.testing.extension.SocksProxyResource; -import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; -import org.apache.hc.core5.testing.extension.classic.HttpServerResource; -import org.apache.hc.core5.util.Timeout; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.extension.RegisterExtension; - -abstract class ClassicHttp1SocksProxyCoreTransportTest extends ClassicHttpCoreTransportTest { - - private static final Timeout TIMEOUT = Timeout.ofMinutes(1); - - @RegisterExtension - @Order(-Integer.MAX_VALUE) - private final SocksProxyResource proxyResource; - @RegisterExtension - private final HttpServerResource serverResource; - @RegisterExtension - private final HttpRequesterResource clientResource; - - public ClassicHttp1SocksProxyCoreTransportTest(final URIScheme scheme) { - super(scheme); - this.proxyResource = new SocksProxyResource(); - this.serverResource = new HttpServerResource(scheme, bootstrap -> bootstrap - .setSocketConfig(SocketConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setRequestRouter(RequestRouter.builder() - .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", new EchoHandler()) - .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) - .build()) - .addFilterBefore(StandardFilter.MAIN_HANDLER.name(), "no-keep-alive", (request, responseTrigger, context, chain) -> - chain.proceed(request, new HttpFilterChain.ResponseTrigger() { - - @Override - public void sendInformation( - final ClassicHttpResponse response) throws HttpException, IOException { - responseTrigger.sendInformation(response); - } - - @Override - public void submitResponse( - final ClassicHttpResponse response) throws HttpException, IOException { - if (request.getPath().startsWith("/no-keep-alive")) { - response.setHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE); - } - responseTrigger.submitResponse(response); - } - - }, context))); - this.clientResource = new HttpRequesterResource(bootstrap -> bootstrap - .setSocketConfig(SocketConfig.custom() - .setSocksProxyAddress(proxyResource.proxy().getProxyAddress()) - .setSoTimeout(TIMEOUT) - .build())); - } - - @Override - HttpServer serverStart() throws IOException { - return serverResource.start(); - } - - @Override - HttpRequester clientStart() throws IOException { - return clientResource.start(); - } - -} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java index e92582efd6..22f27425cd 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java @@ -65,6 +65,7 @@ import org.apache.hc.core5.http.protocol.RequestConnControl; import org.apache.hc.core5.http.protocol.RequestContent; import org.apache.hc.core5.http.protocol.RequestExpectContinue; +import org.apache.hc.core5.http.protocol.RequestTE; import org.apache.hc.core5.http.protocol.RequestTargetHost; import org.apache.hc.core5.http.protocol.RequestUserAgent; import org.apache.hc.core5.testing.extension.classic.ClassicTestResources; @@ -638,7 +639,8 @@ void testHttpPostNoContentLength() throws Exception { RequestTargetHost.INSTANCE, RequestConnControl.INSTANCE, RequestUserAgent.INSTANCE, - RequestExpectContinue.INSTANCE)); + RequestExpectContinue.INSTANCE, + RequestTE.INSTANCE)); client.start(); final HttpCoreContext context = HttpCoreContext.create(); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTests.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTests.java index fa90c09177..2e04487b19 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTests.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTests.java @@ -103,24 +103,4 @@ public HttpFilters() { } - @Nested - @DisplayName("Core transport (SOCKS)") - class CoreTransportSocksProxy extends ClassicHttp1SocksProxyCoreTransportTest { - - public CoreTransportSocksProxy() { - super(URIScheme.HTTP); - } - - } - - @Nested - @DisplayName("Core transport (TLS, SOCKS)") - class CoreTransportSocksProxyTls extends ClassicHttp1SocksProxyCoreTransportTest { - - public CoreTransportSocksProxyTls() { - super(URIScheme.HTTPS); - } - - } - } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java index ef191c3c8f..435df746b5 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicServerBootstrapFilterTest.java @@ -49,6 +49,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; import org.apache.hc.core5.testing.extension.classic.HttpServerResource; import org.apache.hc.core5.util.Timeout; @@ -70,7 +71,9 @@ abstract class ClassicServerBootstrapFilterTest { public ClassicServerBootstrapFilterTest(final URIScheme scheme) { this.scheme = scheme; - this.serverResource = new HttpServerResource(scheme, bootstrap -> bootstrap + this.serverResource = new HttpServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setSslContext(scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .build()) @@ -96,7 +99,9 @@ public void submitResponse( }, context))); - this.clientResource = new HttpRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setSslContext(SSLTestContexts.createClientSSLContext()) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .build())); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicTLSIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicTLSIntegrationTest.java index 6daf502959..f47c83d5cd 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicTLSIntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicTLSIntegrationTest.java @@ -317,7 +317,7 @@ void testHostNameVerification() throws Exception { .create(); final HttpCoreContext context = HttpCoreContext.create(); - final HttpHost target1 = new HttpHost("https", InetAddress.getLocalHost(), "localhost", server.getLocalPort()); + final HttpHost target1 = new HttpHost("https", InetAddress.getLoopbackAddress(), "localhost", server.getLocalPort()); final ClassicHttpRequest request1 = new BasicClassicHttpRequest(Method.POST, "/stuff"); request1.setEntity(new StringEntity("some stuff", ContentType.TEXT_PLAIN)); try (final ClassicHttpResponse response1 = requester.execute(target1, request1, TIMEOUT, context)) { @@ -325,7 +325,7 @@ void testHostNameVerification() throws Exception { } Assertions.assertThrows(SSLHandshakeException.class, () -> { - final HttpHost target2 = new HttpHost("https", InetAddress.getLocalHost(), "some-other-host", server.getLocalPort()); + final HttpHost target2 = new HttpHost("https", InetAddress.getLoopbackAddress(), "some-other-host", server.getLocalPort()); final ClassicHttpRequest request2 = new BasicClassicHttpRequest(Method.POST, "/stuff"); request2.setEntity(new StringEntity("some stuff", ContentType.TEXT_PLAIN)); try (final ClassicHttpResponse response2 = requester.execute(target2, request2, TIMEOUT, context)) { diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java index 38d25ea63f..81ff0e8045 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java @@ -49,6 +49,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; import org.apache.hc.core5.testing.extension.classic.HttpServerResource; import org.apache.hc.core5.util.Timeout; @@ -72,8 +73,9 @@ abstract class MonitoringResponseOutOfOrderStrategyIntegrationTest { public MonitoringResponseOutOfOrderStrategyIntegrationTest(final URIScheme scheme) { this.scheme = scheme; - - this.serverResource = new HttpServerResource(scheme, bootstrap -> bootstrap + this.serverResource = new HttpServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setSslContext(scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .setSndBufSize(BUFFER_SIZE) @@ -88,7 +90,9 @@ public MonitoringResponseOutOfOrderStrategyIntegrationTest(final URIScheme schem .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build())); - this.clientResource = new HttpRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setSslContext(SSLTestContexts.createClientSSLContext()) .setSocketConfig(SocketConfig.custom() .setSoTimeout(TIMEOUT) .setRcvBufSize(BUFFER_SIZE) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ApacheHttpDCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ApacheHttpDCompatIT.java new file mode 100644 index 0000000000..78cbd98f7b --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ApacheHttpDCompatIT.java @@ -0,0 +1,128 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.testing.compatibility.classic.ClassicHttpCompatTest; +import org.apache.hc.core5.testing.compatibility.nio.AsyncHttp1CompatTest; +import org.apache.hc.core5.testing.compatibility.nio.AsyncHttp2CompatTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class ApacheHttpDCompatIT { + + private static Network NETWORK = Network.newNetwork(); + @Container + static final GenericContainer HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK); + + @AfterAll + static void cleanup() { + HTTPD_CONTAINER.close(); + } + + static HttpHost targetContainerHost() { + return new HttpHost(URIScheme.HTTP.id, HTTPD_CONTAINER.getHost(), HTTPD_CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); + } + + static HttpHost targetContainerH2CHost() { + return new HttpHost(URIScheme.HTTP.id, HTTPD_CONTAINER.getHost(), HTTPD_CONTAINER.getMappedPort(ContainerImages.H2C_PORT)); + } + + static HttpHost targetContainerTLSHost() { + return new HttpHost(URIScheme.HTTPS.id, HTTPD_CONTAINER.getHost(), HTTPD_CONTAINER.getMappedPort(ContainerImages.HTTPS_PORT)); + } + + @Nested + @DisplayName("Classic, HTTP/1, plain") + class ClassicHttp1 extends ClassicHttpCompatTest { + + public ClassicHttp1() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Classic, HTTP/1, TLS") + class ClassicHttp1Tls extends ClassicHttpCompatTest { + + public ClassicHttp1Tls() throws Exception { + super(targetContainerTLSHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/1, plain") + class AsyncHttp1 extends AsyncHttp1CompatTest { + + public AsyncHttp1() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/2, plain") + class AsyncHttp2 extends AsyncHttp2CompatTest { + + public AsyncHttp2() throws Exception { + super(targetContainerH2CHost(), HttpVersionPolicy.FORCE_HTTP_2); + } + + } + + @Nested + @DisplayName("Async, HTTP/1, TLS") + class AsyncHttp1Tls extends AsyncHttp1CompatTest { + + public AsyncHttp1Tls() throws Exception { + super(targetContainerTLSHost()); + } + + } + + @Nested + @DisplayName("Async, protocol HTTP/2, TLS") + class AsyncHttp2Tls extends AsyncHttp2CompatTest { + + public AsyncHttp2Tls() throws Exception { + super(targetContainerTLSHost(), HttpVersionPolicy.FORCE_HTTP_2); + } + + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java new file mode 100644 index 0000000000..ae402e73f5 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/ContainerImages.java @@ -0,0 +1,199 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.testing.compatibility; + +import java.util.Random; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; + +public final class ContainerImages { + + private static final Logger LOG = LoggerFactory.getLogger(ContainerImages.class); + + public static final String AAA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + public static final String BBB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + public static final String CCC = "ccccccccccccccccccccccccccccccccccccccccccccccccc"; + public static final String PUSHY = "I am being very pushy"; + + public static final String APACHE_HTTPD = "test-apache"; + public static final String NGINX = "test-nginx"; + public static final String HTTPBIN = "test-httpbin"; + public static final String DANTE = "test-dante"; + public static final String JETTY = "test-jetty"; + + public static int HTTP_PORT = 80; + public static int H2C_PORT = 81; + public static int HTTPS_PORT = 443; + public static int SOCKS_PORT = 1080; + public static int HTTP_EXT_PORT = 8080; + public static int HTTPS_EXT_PORT = 8443; + + static byte[] blob() { + final Random random = new Random(System.currentTimeMillis()); + final byte[] bytes = new byte[5 * 1024 * 1024 + random.nextInt(102400)]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) ('0' + random.nextInt(10)); + } + return bytes; + } + + public static GenericContainer httpBin(final Network network) { + return new GenericContainer<>(DockerImageName.parse("kennethreitz/httpbin:latest")) + .withNetwork(network) + .withNetworkAliases(HTTPBIN) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(HTTP_PORT); + } + + public static GenericContainer apacheHttpD(final Network network) { + return new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("httpd-default.conf", "docker/httpd/httpd-default.conf") + .withFileFromClasspath("httpd-h2c.conf", "docker/httpd/httpd-h2c.conf") + .withFileFromClasspath("httpd-ssl.conf", "docker/httpd/httpd-ssl.conf") + .withFileFromClasspath("server-cert.pem", "docker/server-cert.pem") + .withFileFromClasspath("server-key.pem", "docker/server-key.pem") + .withFileFromString("pushy", PUSHY) + .withFileFromString("aaa", AAA) + .withFileFromString("bbb", BBB) + .withFileFromString("ccc", CCC) + .withFileFromTransferable("blob", Transferable.of(blob())) + .withDockerfileFromBuilder(builder -> builder + .from("httpd:2.4") + .env("var_dir", "/var/httpd") + .env("www_dir", "${var_dir}/www") + .run("mkdir -p ${var_dir}") + .run("mkdir -p ${www_dir}") + .run("echo '\\n" + + "LoadModule http2_module modules/mod_http2.so\\n" + + "LoadModule ssl_module modules/mod_ssl.so\\n" + + "Include conf/extra/httpd-default.conf\\n" + + "Include conf/extra/httpd-h2c.conf\\n" + + "Include conf/extra/httpd-ssl.conf\\n" + + "'" + + " >> /usr/local/apache2/conf/httpd.conf") + .copy("httpd-default.conf", "/usr/local/apache2/conf/extra/httpd-default.conf") + .copy("httpd-h2c.conf", "/usr/local/apache2/conf/extra/httpd-h2c.conf") + .copy("httpd-ssl.conf", "/usr/local/apache2/conf/extra/httpd-ssl.conf") + .copy("server-cert.pem", "/usr/local/apache2/conf/server-cert.pem") + .copy("server-key.pem", "/usr/local/apache2/conf/server-key.pem") + .copy("pushy", "${www_dir}/") + .copy("aaa", "${www_dir}/") + .copy("bbb", "${www_dir}/") + .copy("ccc", "${www_dir}/") + .copy("blob", "${www_dir}/") + .build())) + .withNetwork(network) + .withNetworkAliases(APACHE_HTTPD) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(HTTP_PORT, H2C_PORT, HTTPS_PORT); + } + + public static GenericContainer nginx(final Network network) { + return new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("default.conf", "docker/nginx/default.conf") + .withFileFromClasspath("h2c.conf", "docker/nginx/h2c.conf") + .withFileFromClasspath("ssl.conf", "docker/nginx/ssl.conf") + .withFileFromClasspath("server-cert.pem", "docker/server-cert.pem") + .withFileFromClasspath("server-key.pem", "docker/server-key.pem") + .withFileFromString("pushy", PUSHY) + .withFileFromString("aaa", AAA) + .withFileFromString("bbb", BBB) + .withFileFromString("ccc", CCC) + .withFileFromTransferable("blob", Transferable.of(blob())) + .withDockerfileFromBuilder(builder -> builder + .from("nginx:1.23") + .env("var_dir", "/var/nginx") + .env("www_dir", "${var_dir}/www") + .run("mkdir -p ${var_dir}") + .copy("default.conf", "/etc/nginx/conf.d/default.conf") + .copy("h2c.conf", "/etc/nginx/conf.d/h2c.conf") + .copy("ssl.conf", "/etc/nginx/conf.d/ssl.conf") + .copy("server-cert.pem", "/etc/nginx/server-cert.pem") + .copy("server-key.pem", "/etc/nginx/server-key.pem") + .copy("pushy", "${www_dir}/") + .copy("aaa", "${www_dir}/") + .copy("bbb", "${www_dir}/") + .copy("ccc", "${www_dir}/") + .copy("blob", "${www_dir}/") + .build())) + .withNetwork(network) + .withNetworkAliases(NGINX) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(HTTP_PORT, H2C_PORT, HTTPS_PORT); + } + + public static GenericContainer dante(final Network network) { + return new GenericContainer<>(new ImageFromDockerfile() + .withDockerfileFromBuilder(builder -> builder + .from("vimagick/dante:latest") + .run("useradd socks") + .run("echo socks:nopassword | chpasswd") + .build())) + .withNetwork(network) + .withNetworkAliases(DANTE) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(SOCKS_PORT); + } + + public static GenericContainer jetty(final Network network) { + return new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("server.p12", "docker/server.p12") + .withFileFromString("aaa", AAA) + .withFileFromString("bbb", BBB) + .withFileFromString("ccc", CCC) + .withFileFromTransferable("blob", Transferable.of(blob())) + .withDockerfileFromBuilder(builder -> builder + .from("jetty:12.0-jdk17-amazoncorretto") + .env("jetty_base", "/var/lib/jetty/") + .env("webapp_root", "${jetty_base}/webapps/ROOT") + .env("uid", "jetty") + .user("root") + .copy("server.p12", "${jetty_base}/etc/keystore.p12") + .run("echo 'jetty.sslContext.keyStorePassword=nopassword' >> ${jetty_base}/start.d/ssl.ini") + .run("mkdir -p ${webapp_root}/") + .copy("aaa", "${webapp_root}/") + .copy("bbb", "${webapp_root}/") + .copy("ccc", "${webapp_root}/") + .copy("blob", "${webapp_root}/") + .run("chown -R ${uid}:${uid} ${jetty_base}/") + .user("${uid}:${uid}") + .run("java -jar ${JETTY_HOME}/start.jar --add-modules=http2 --add-modules=ee10-deploy --approve-all-licenses") + .build())) + .withNetwork(network) + .withNetworkAliases(JETTY) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(HTTP_EXT_PORT, HTTPS_EXT_PORT); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/DanteHttpBinCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/DanteHttpBinCompatIT.java new file mode 100644 index 0000000000..f687e61477 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/DanteHttpBinCompatIT.java @@ -0,0 +1,91 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.testing.compatibility.classic.SocksHttpBinClassicCompatTest; +import org.apache.hc.core5.testing.compatibility.nio.SocksHttpBinAsyncCompatTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class DanteHttpBinCompatIT { + + private static Network NETWORK = Network.newNetwork(); + @Container + static final GenericContainer DANTE_CONTAINER = ContainerImages.dante(NETWORK); + @Container + static final GenericContainer HTTP_BIN_CONTAINER = ContainerImages.httpBin(NETWORK); + + @AfterAll + static void cleanup() { + DANTE_CONTAINER.close(); + HTTP_BIN_CONTAINER.close(); + } + + static SocketAddress socketContainerAddress() { + return new InetSocketAddress(DANTE_CONTAINER.getHost(), DANTE_CONTAINER.getMappedPort(ContainerImages.SOCKS_PORT)); + } + + static HttpHost targetInternalHost() { + return new HttpHost(URIScheme.HTTP.id, ContainerImages.HTTPBIN, ContainerImages.HTTP_PORT); + } + + static String SOCKS_USER = "socks"; + static String SOCKS_PW = "nopassword"; + + @Nested + @DisplayName("Classic, SOCKS proxy") + class Classic extends SocksHttpBinClassicCompatTest { + + public Classic() throws Exception { + super(targetInternalHost(), socketContainerAddress(), SOCKS_USER, SOCKS_PW); + } + + } + + @Nested + @DisplayName("Async, SOCKS proxy") + class Async extends SocksHttpBinAsyncCompatTest { + + public Async() throws Exception { + super(targetInternalHost(), socketContainerAddress(), SOCKS_USER, SOCKS_PW); + } + + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/HttpBinCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/HttpBinCompatIT.java new file mode 100644 index 0000000000..a59e6f1844 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/HttpBinCompatIT.java @@ -0,0 +1,78 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.testing.compatibility.classic.HttpBinClassicCompatTest; +import org.apache.hc.core5.testing.compatibility.nio.HttpBinAsyncCompatTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class HttpBinCompatIT { + + private static Network NETWORK = Network.newNetwork(); + @Container + static final GenericContainer HTTP_BIN_CONTAINER = ContainerImages.httpBin(NETWORK); + + @AfterAll + static void cleanup() { + HTTP_BIN_CONTAINER.close(); + } + + static HttpHost targetContainerHost() { + return new HttpHost(URIScheme.HTTP.id, HTTP_BIN_CONTAINER.getHost(), HTTP_BIN_CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); + } + + @Nested + @DisplayName("Classic") + class Classic extends HttpBinClassicCompatTest { + + public Classic() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Async") + class Async extends HttpBinAsyncCompatTest { + + public Async() throws Exception { + super(targetContainerHost()); + } + + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/JettyCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/JettyCompatIT.java new file mode 100644 index 0000000000..2a34b0b7ff --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/JettyCompatIT.java @@ -0,0 +1,94 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.testing.compatibility.classic.ClassicHttpCompatTest; +import org.apache.hc.core5.testing.compatibility.nio.AsyncHttp1CompatTest; +import org.apache.hc.core5.testing.compatibility.nio.AsyncHttp2CompatNoPushTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class JettyCompatIT { + + private static Network NETWORK = Network.newNetwork(); + @Container + static final GenericContainer JETTY_CONTAINER = ContainerImages.jetty(NETWORK); + + @AfterAll + static void cleanup() { + JETTY_CONTAINER.close(); + } + + static HttpHost targetContainerHost() { + return new HttpHost(URIScheme.HTTP.id, JETTY_CONTAINER.getHost(), JETTY_CONTAINER.getMappedPort(ContainerImages.HTTP_EXT_PORT)); + } + + static HttpHost targetContainerTLSHost() { + return new HttpHost(URIScheme.HTTPS.id, JETTY_CONTAINER.getHost(), JETTY_CONTAINER.getMappedPort(ContainerImages.HTTPS_EXT_PORT)); + } + + @Nested + @DisplayName("Classic, HTTP/1, plain") + class ClassicHttp1 extends ClassicHttpCompatTest { + + public ClassicHttp1() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/1, plain") + class AsyncHttp1 extends AsyncHttp1CompatTest { + + public AsyncHttp1() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/2, TLS") + class AsyncHttp2Tls extends AsyncHttp2CompatNoPushTest { + + public AsyncHttp2Tls() throws Exception { + super(targetContainerTLSHost(), HttpVersionPolicy.FORCE_HTTP_2); + } + + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/NginxCompatIT.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/NginxCompatIT.java new file mode 100644 index 0000000000..3a8a632b14 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/NginxCompatIT.java @@ -0,0 +1,128 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.testing.compatibility.classic.ClassicHttpCompatTest; +import org.apache.hc.core5.testing.compatibility.nio.AsyncHttp1CompatTest; +import org.apache.hc.core5.testing.compatibility.nio.AsyncHttp2CompatTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers(disabledWithoutDocker = true) +class NginxCompatIT { + + private static Network NETWORK = Network.newNetwork(); + @Container + static final GenericContainer NGINX_CONTAINER = ContainerImages.nginx(NETWORK); + + @AfterAll + static void cleanup() { + NGINX_CONTAINER.close(); + } + + static HttpHost targetContainerHost() { + return new HttpHost(URIScheme.HTTP.id, NGINX_CONTAINER.getHost(), NGINX_CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); + } + + static HttpHost targetContainerH2CHost() { + return new HttpHost(URIScheme.HTTP.id, NGINX_CONTAINER.getHost(), NGINX_CONTAINER.getMappedPort(ContainerImages.H2C_PORT)); + } + + static HttpHost targetContainerTLSHost() { + return new HttpHost(URIScheme.HTTPS.id, NGINX_CONTAINER.getHost(), NGINX_CONTAINER.getMappedPort(ContainerImages.HTTPS_PORT)); + } + + @Nested + @DisplayName("Classic, HTTP/1, plain") + class ClassicHttp1 extends ClassicHttpCompatTest { + + public ClassicHttp1() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Classic, HTTP/1, TLS") + class ClassicHttp1Tls extends ClassicHttpCompatTest { + + public ClassicHttp1Tls() throws Exception { + super(targetContainerTLSHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/1, plain") + class AsyncHttp1 extends AsyncHttp1CompatTest { + + public AsyncHttp1() throws Exception { + super(targetContainerHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/2, plain") + class AsyncHttp2 extends AsyncHttp2CompatTest { + + public AsyncHttp2() throws Exception { + super(targetContainerH2CHost(), HttpVersionPolicy.FORCE_HTTP_2); + } + + } + + @Nested + @DisplayName("Async, HTTP/1, TLS") + class AsyncHttp1Tls extends AsyncHttp1CompatTest { + + public AsyncHttp1Tls() throws Exception { + super(targetContainerTLSHost()); + } + + } + + @Nested + @DisplayName("Async, HTTP/2, TLS") + class AsyncHttp2Tls extends AsyncHttp2CompatTest { + + public AsyncHttp2Tls() throws Exception { + super(targetContainerTLSHost(), HttpVersionPolicy.FORCE_HTTP_2); + } + + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/TLSTestContexts.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/TLSTestContexts.java new file mode 100644 index 0000000000..527f90aa75 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/TLSTestContexts.java @@ -0,0 +1,56 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility; + +import java.io.IOException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.SSLContext; + +import org.apache.hc.core5.ssl.SSLContextBuilder; + +public final class TLSTestContexts { + + public static SSLContext createClientSSLContext() { + final URL keyStoreURL = TLSTestContexts.class.getResource("/test-ca.jks"); + final String storePassword = "nopassword"; + try { + return SSLContextBuilder.create() + .loadTrustMaterial(keyStoreURL, storePassword.toCharArray()) + .build(); + } catch (final NoSuchAlgorithmException | KeyManagementException | KeyStoreException | CertificateException | + IOException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/ClassicHttpCompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/ClassicHttpCompatTest.java new file mode 100644 index 0000000000..87651a688e --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/ClassicHttpCompatTest.java @@ -0,0 +1,154 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.classic; + +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.impl.bootstrap.HttpRequester; +import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.testing.Result; +import org.apache.hc.core5.testing.compatibility.ContainerImages; +import org.apache.hc.core5.testing.compatibility.TLSTestContexts; +import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; +import org.apache.hc.core5.util.Timeout; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class ClassicHttpCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + private final HttpHost target; + @RegisterExtension + private final HttpRequesterResource clientResource; + + public ClassicHttpCompatTest(final HttpHost target) { + this.target = target; + this.clientResource = new HttpRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setSocketConfig(SocketConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setSslContext(TLSTestContexts.createClientSSLContext()) + ); + } + + void configure(final Consumer customizer) { + clientResource.configure(customizer); + } + + HttpRequester client() { + return clientResource.start(); + } + + @Test + void test_sequential_requests() throws Exception { + final HttpRequester requester = client(); + + final int n = 20; + for (int i = 0; i < n; i++) { + final HttpCoreContext context = HttpCoreContext.create(); + final ClassicHttpRequest request = ClassicRequestBuilder.get("/aaa") + .setHttpHost(target) + .build(); + requester.execute(target, request, TIMEOUT, context, response -> { + assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK)); + final String body1 = EntityUtils.toString(response.getEntity()); + assertThat(body1, CoreMatchers.equalTo(ContainerImages.AAA)); + return null; + }); + } + } + + @Test + void test_multi_threaded_requests() throws Exception { + final HttpRequester requester = client(); + + final int c = 10; + final AtomicInteger n = new AtomicInteger(20 * c); + final CountDownLatch countDownLatch = new CountDownLatch(c); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + final ExecutorService executorService = Executors.newFixedThreadPool(c); + try { + for (int i = 0; i < c; i++) { + executorService.execute(() -> { + try { + while (n.decrementAndGet() > 0) { + final HttpCoreContext context = HttpCoreContext.create(); + final ClassicHttpRequest request = ClassicRequestBuilder.get("/aaa") + .setHttpHost(target) + .build(); + try { + requester.execute(target, request, TIMEOUT, context, response -> { + resultQueue.add(new Result<>( + request, + response, + EntityUtils.toString(response.getEntity()))); + return null; + }); + } catch (final Exception ex) { + resultQueue.add(new Result<>(request, ex)); + } + } + } finally { + countDownLatch.countDown(); + } + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + } else { + Assertions.fail(result.exception); + } + } + } finally { + executorService.shutdownNow(); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/HttpBinClassicCompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/HttpBinClassicCompatTest.java new file mode 100644 index 0000000000..bf447fd0ed --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/HttpBinClassicCompatTest.java @@ -0,0 +1,121 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.classic; + +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.impl.bootstrap.HttpRequester; +import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.testing.extension.classic.HttpRequesterResource; +import org.apache.hc.core5.util.Timeout; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class HttpBinClassicCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + private final HttpHost target; + @RegisterExtension + private final HttpRequesterResource clientResource; + + public HttpBinClassicCompatTest(final HttpHost target) { + this.target = target; + this.clientResource = new HttpRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setSocketConfig(SocketConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + ); + } + + void configure(final Consumer customizer) { + clientResource.configure(customizer); + } + + HttpRequester client() { + return clientResource.start(); + } + + @Test + void test_sequential_request_execution() throws Exception { + final HttpRequester client = client(); + final List requestMessages = Arrays.asList( + ClassicRequestBuilder.get("/headers") + .setHttpHost(target) + .build(), + ClassicRequestBuilder.post("/anything") + .setHttpHost(target) + .setEntity(new StringEntity("some important message", ContentType.TEXT_PLAIN)) + .build(), + ClassicRequestBuilder.put("/anything") + .setHttpHost(target) + .setEntity(new StringEntity("some important message", ContentType.TEXT_PLAIN)) + .build(), + ClassicRequestBuilder.get("/drip") + .setHttpHost(target) + .build(), + ClassicRequestBuilder.get("/bytes/20000") + .setHttpHost(target) + .build(), + ClassicRequestBuilder.get("/delay/2") + .setHttpHost(target) + .build(), + ClassicRequestBuilder.post("/delay/2") + .setHttpHost(target) + .setEntity(new StringEntity("some important message", ContentType.TEXT_PLAIN)) + .build(), + ClassicRequestBuilder.put("/delay/2") + .setHttpHost(target) + .setEntity(new StringEntity("some important message", ContentType.TEXT_PLAIN)) + .build() + ); + + for (final ClassicHttpRequest request : requestMessages) { + final HttpCoreContext context = HttpCoreContext.create(); + client.execute(target, request, TIMEOUT, context, response -> { + assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK)); + return null; + }); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/SocksHttpBinClassicCompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/SocksHttpBinClassicCompatTest.java new file mode 100644 index 0000000000..e5c77e1153 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/classic/SocksHttpBinClassicCompatTest.java @@ -0,0 +1,56 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.classic; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.SocketAddress; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.SocketConfig; + +public abstract class SocksHttpBinClassicCompatTest extends HttpBinClassicCompatTest { + + public SocksHttpBinClassicCompatTest(final HttpHost target, final SocketAddress socksProxy, final String socksUser, final String socksPassword) { + super(target); + configure(bootstrap -> bootstrap + .setSocketConfig(SocketConfig.custom() + .setSocksProxyAddress(socksProxy) + .setSoTimeout(TIMEOUT) + .build()) + ); + Authenticator.setDefault(new Authenticator() { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(socksUser, socksPassword.toCharArray()); + } + + }); + } +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java deleted file mode 100644 index e2ca5c66fa..0000000000 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/H2CompatibilityTest.java +++ /dev/null @@ -1,437 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ -package org.apache.hc.core5.testing.compatibility.http2; - -import java.io.IOException; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeoutException; - -import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.Message; -import org.apache.hc.core5.http.Method; -import org.apache.hc.core5.http.config.CharCodingConfig; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.message.BasicHttpRequest; -import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.AsyncEntityProducer; -import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; -import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler; -import org.apache.hc.core5.http.nio.support.BasicRequestProducer; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; -import org.apache.hc.core5.http2.HttpVersionPolicy; -import org.apache.hc.core5.http2.config.H2Config; -import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; -import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; -import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; -import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; -import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; -import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; -import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.apache.hc.core5.util.TextUtils; -import org.apache.hc.core5.util.Timeout; - -public class H2CompatibilityTest { - - private final HttpAsyncRequester client; - - public static void main(final String... args) throws Exception { - - final HttpHost[] h2servers = new HttpHost[]{ - new HttpHost("http", "localhost", 8080), - new HttpHost("http", "localhost", 8081) - }; - - final HttpHost httpbin = new HttpHost("http", "localhost", 8082); - - final H2CompatibilityTest test = new H2CompatibilityTest(); - try { - test.start(); - for (final HttpHost h2server : h2servers) { - test.executeH2(h2server); - } - test.executeHttpBin(httpbin); - } finally { - test.shutdown(); - } - } - - H2CompatibilityTest() { - this.client = H2RequesterBootstrap.bootstrap() - .setIOReactorConfig(IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setH2Config(H2Config.custom() - .setPushEnabled(true) - .build()) - .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT) - .setStreamListener(LoggingH2StreamListener.INSTANCE) - .setConnPoolListener(LoggingConnPoolListener.INSTANCE) - .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) - .setExceptionCallback(LoggingExceptionCallback.INSTANCE) - .setIOSessionListener(LoggingIOSessionListener.INSTANCE) - .create(); - } - - void start() { - client.start(); - } - - void shutdown() { - client.close(CloseMode.GRACEFUL); - } - - private static final Timeout TIMEOUT = Timeout.ofSeconds(5); - - void executeH2(final HttpHost target) throws Exception { - { - System.out.println("*** HTTP/2 simple request execution ***"); - final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); - try { - final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - - final CountDownLatch countDownLatch = new CountDownLatch(1); - final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html"); - endpoint.execute( - new BasicRequestProducer(httpget, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, httpget, response, - Objects.toString(response.getFirstHeader("server"))); - } else { - logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, httpget, null, "(cancelled)"); - countDownLatch.countDown(); - } - - }); - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(single request execution failed to complete in time)"); - } - } catch (final ExecutionException ex) { - final Throwable cause = ex.getCause(); - logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")"); - } catch (final TimeoutException ex) { - logResult(TestResult.NOK, target, null, null, "(time out)"); - } - } - { - System.out.println("*** HTTP/2 multiplexed request execution ***"); - final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); - try { - final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - - final int reqCount = 20; - final CountDownLatch countDownLatch = new CountDownLatch(reqCount); - for (int i = 0; i < reqCount; i++) { - final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html"); - endpoint.execute( - new BasicRequestProducer(httpget, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, httpget, response, - "multiplexed / " + response.getFirstHeader("server")); - } else { - logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, httpget, null, "(cancelled)"); - countDownLatch.countDown(); - } - - }); - } - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(multiplexed request execution failed to complete in time)"); - } - } catch (final ExecutionException ex) { - final Throwable cause = ex.getCause(); - logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")"); - } catch (final TimeoutException ex) { - logResult(TestResult.NOK, target, null, null, "(time out)"); - } - } - { - System.out.println("*** HTTP/2 request execution with push ***"); - final Future connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null); - try { - final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - - final CountDownLatch countDownLatch = new CountDownLatch(5); - final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/index.html"); - final Future> future = endpoint.execute( - new BasicRequestProducer(httpget, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - (request, context) -> new AbstractAsyncPushHandler>( - new BasicResponseConsumer<>(new DiscardingEntityConsumer<>())) { - - @Override - protected void handleResponse( - final HttpRequest promise, - final Message responseMessage) throws IOException, HttpException { - final HttpResponse response = responseMessage.getHead(); - logResult(TestResult.OK, target, promise, response, - "pushed / " + response.getFirstHeader("server")); - countDownLatch.countDown(); - } - - @Override - protected void handleError( - final HttpRequest promise, - final Exception cause) { - logResult(TestResult.NOK, target, promise, null, "(" + cause.getMessage() + ")"); - countDownLatch.countDown(); - } - }, - null, - null); - final Message message = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - final HttpResponse response = message.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, httpget, response, - Objects.toString(response.getFirstHeader("server"))); - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "Push messages not received"); - } - } else { - logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")"); - } - } catch (final ExecutionException ex) { - final Throwable cause = ex.getCause(); - logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")"); - } catch (final TimeoutException ex) { - logResult(TestResult.NOK, target, null, null, "(time out)"); - } - } - } - - void executeHttpBin(final HttpHost target) throws Exception { - { - System.out.println("*** httpbin.org HTTP/1.1 simple request execution ***"); - - final List> requestMessages = Arrays.asList( - new Message<>(new BasicHttpRequest(Method.GET, target, "/headers")), - new Message<>( - new BasicHttpRequest(Method.POST, target, "/anything"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), - new Message<>( - new BasicHttpRequest(Method.PUT, target, "/anything"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), - new Message<>(new BasicHttpRequest(Method.GET, target, "/drip")), - new Message<>(new BasicHttpRequest(Method.GET, target, "/bytes/20000")), - new Message<>(new BasicHttpRequest(Method.GET, target, "/delay/2")), - new Message<>( - new BasicHttpRequest(Method.POST, target, "/delay/2"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), - new Message<>( - new BasicHttpRequest(Method.PUT, target, "/delay/2"), - new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)) - ); - - for (final Message message : requestMessages) { - final CountDownLatch countDownLatch = new CountDownLatch(1); - final HttpRequest request = message.getHead(); - final AsyncEntityProducer entityProducer = message.getBody(); - client.execute( - new BasicRequestProducer(request, entityProducer), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() - .setCharset(StandardCharsets.US_ASCII) - .setMalformedInputAction(CodingErrorAction.IGNORE) - .setUnmappableInputAction(CodingErrorAction.REPLACE) - .build())), - TIMEOUT, - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, request, response, - Objects.toString(response.getFirstHeader("server"))); - } else { - logResult(TestResult.NOK, target, request, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, request, null, "(cancelled)"); - countDownLatch.countDown(); - } - }); - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)"); - } - } - } - { - System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***"); - - final Future connectFuture = client.connect(target, TIMEOUT); - final AsyncClientEndpoint streamEndpoint = connectFuture.get(); - - final int n = 10; - final CountDownLatch countDownLatch = new CountDownLatch(n); - for (int i = 0; i < n; i++) { - - final HttpRequest request; - final AsyncEntityProducer entityProducer; - if (i % 2 == 0) { - request = new BasicHttpRequest(Method.GET, target, "/headers"); - entityProducer = null; - } else { - request = new BasicHttpRequest(Method.POST, target, "/anything"); - entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN); - } - - streamEndpoint.execute( - new BasicRequestProducer(request, entityProducer), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() - .setCharset(StandardCharsets.US_ASCII) - .setMalformedInputAction(CodingErrorAction.IGNORE) - .setUnmappableInputAction(CodingErrorAction.REPLACE) - .build())), - new FutureCallback>() { - - @Override - public void completed(final Message responseMessage) { - final HttpResponse response = responseMessage.getHead(); - final int code = response.getCode(); - if (code == HttpStatus.SC_OK) { - logResult(TestResult.OK, target, request, response, - "pipelined / " + response.getFirstHeader("server")); - } else { - logResult(TestResult.NOK, target, request, response, "(status " + code + ")"); - } - countDownLatch.countDown(); - } - - @Override - public void failed(final Exception ex) { - logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")"); - countDownLatch.countDown(); - } - - @Override - public void cancelled() { - logResult(TestResult.NOK, target, request, null, "(cancelled)"); - countDownLatch.countDown(); - } - }); - } - if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) { - logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)"); - } - } - } - - enum TestResult {OK, NOK} - - private void logResult( - final TestResult result, - final HttpHost httpHost, - final HttpRequest request, - final HttpResponse response, - final String message) { - final StringBuilder buf = new StringBuilder(); - buf.append(result); - if (buf.length() == 2) { - buf.append(" "); - } - buf.append(": ").append(httpHost).append(" "); - if (response != null) { - buf.append(response.getVersion()).append(" "); - } - if (request != null) { - buf.append(request.getMethod()).append(" ").append(request.getRequestUri()); - } - if (message != null && !TextUtils.isBlank(message)) { - buf.append(" -> ").append(message); - } - System.out.println(buf); - } - -} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp1CompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp1CompatTest.java new file mode 100644 index 0000000000..2ddd89ec48 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp1CompatTest.java @@ -0,0 +1,70 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.nio; + +import java.util.function.Consumer; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2AsyncRequester; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.compatibility.TLSTestContexts; +import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AsyncHttp1CompatTest extends AsyncHttpCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + @RegisterExtension + private final H2AsyncRequesterResource clientResource; + + public AsyncHttp1CompatTest(final HttpHost target) { + super(target); + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setTlsStrategy(new BasicClientTlsStrategy(TLSTestContexts.createClientSSLContext())) + .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)); + } + + void configure(final Consumer customizer) { + clientResource.configure(customizer); + } + + @Override + H2AsyncRequester client() { + return clientResource.start(); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp2CompatNoPushTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp2CompatNoPushTest.java new file mode 100644 index 0000000000..b39efee0c7 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp2CompatNoPushTest.java @@ -0,0 +1,76 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.nio; + +import java.util.function.Consumer; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2AsyncRequester; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.compatibility.TLSTestContexts; +import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AsyncHttp2CompatNoPushTest extends AsyncHttpCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + private final HttpHost target; + @RegisterExtension + private final H2AsyncRequesterResource clientResource; + + public AsyncHttp2CompatNoPushTest(final HttpHost target, final HttpVersionPolicy versionPolicy) { + super(target); + this.target = target; + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setH2Config(H2Config.custom() + .setPushEnabled(true) + .build()) + .setTlsStrategy(new H2ClientTlsStrategy(TLSTestContexts.createClientSSLContext())) + .setVersionPolicy(versionPolicy)); + } + + void configure(final Consumer customizer) { + clientResource.configure(customizer); + } + + @Override + H2AsyncRequester client() { + return clientResource.start(); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp2CompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp2CompatTest.java new file mode 100644 index 0000000000..64c1d616c8 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttp2CompatTest.java @@ -0,0 +1,165 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.nio; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.RequestNotExecutedException; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2AsyncRequester; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.Result; +import org.apache.hc.core5.testing.compatibility.TLSTestContexts; +import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class AsyncHttp2CompatTest extends AsyncHttpCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + private final HttpHost target; + @RegisterExtension + private final H2AsyncRequesterResource clientResource; + + public AsyncHttp2CompatTest(final HttpHost target, final HttpVersionPolicy versionPolicy) { + super(target); + this.target = target; + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setH2Config(H2Config.custom() + .setPushEnabled(true) + .build()) + .setTlsStrategy(new H2ClientTlsStrategy(TLSTestContexts.createClientSSLContext())) + .setVersionPolicy(versionPolicy)); + } + + void configure(final Consumer customizer) { + clientResource.configure(customizer); + } + + @Override + H2AsyncRequester client() { + return clientResource.start(); + } + + @Test + void test_request_execution_with_push() throws Exception { + final H2AsyncRequester client = client(); + final Future connectFuture = client.connect(target, TIMEOUT, null, null); + final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + try { + final CountDownLatch countDownLatch = new CountDownLatch(4); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/pushy"); + endpoint.execute( + new BasicRequestProducer(httpget, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + (request, context) -> + new AbstractAsyncPushHandler>(new BasicResponseConsumer<>(new StringAsyncEntityConsumer())) { + + @Override + protected void handleResponse(final HttpRequest promise, final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + protected void handleError(final HttpRequest promise, final Exception cause) { + resultQueue.add(new Result<>(httpget, cause)); + countDownLatch.countDown(); + } + + }, + null, + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(httpget, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + } else { + Assertions.fail(result.exception); + } + } + } finally { + endpoint.releaseAndDiscard(); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttpCompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttpCompatTest.java new file mode 100644 index 0000000000..e8509f69ff --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/AsyncHttpCompatTest.java @@ -0,0 +1,223 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.nio; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.RequestNotExecutedException; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.testing.Result; +import org.apache.hc.core5.testing.compatibility.ContainerImages; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public abstract class AsyncHttpCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + private final HttpHost target; + + public AsyncHttpCompatTest(final HttpHost target) { + this.target = target; + } + + abstract T client(); + + @Test + void test_multiple_request_execution_over_same_connection() throws Exception { + final HttpAsyncRequester client = client(); + final Future connectFuture = client.connect(target, TIMEOUT, null, null); + final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + try { + final int n = 20; + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < n; i++) { + final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/aaa"); + endpoint.execute( + new BasicRequestProducer(httpget, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(httpget, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + Assertions.assertEquals(ContainerImages.AAA, result.content); + } else { + Assertions.fail(result.exception); + } + } + } finally { + endpoint.releaseAndDiscard(); + } + } + + @Test + void test_multiple_request_execution_over_multiple_connections() throws Exception { + final HttpAsyncRequester client = client(); + final int c = 10; + client.setDefaultMaxPerRoute(10); + final int n = 20 * c; + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < n; i++) { + final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/aaa"); + client.execute( + target, + new BasicRequestProducer(httpget, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + TIMEOUT, + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(httpget, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + Assertions.assertEquals(ContainerImages.AAA, result.content); + } else { + Assertions.fail(result.exception); + } + } + } + + @Test + void test_multiple_request_execution_large_blob() throws Exception { + final HttpAsyncRequester client = client(); + final Future connectFuture = client.connect(target, TIMEOUT, null, null); + final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + try { + final int n = 20; + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < n; i++) { + final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/blob"); + endpoint.execute( + new BasicRequestProducer(httpget, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + httpget, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(httpget, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + } else { + Assertions.fail(result.exception); + } + } + } finally { + endpoint.releaseAndDiscard(); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/HttpBinAsyncCompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/HttpBinAsyncCompatTest.java new file mode 100644 index 0000000000..cf5aca906a --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/HttpBinAsyncCompatTest.java @@ -0,0 +1,200 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.nio; + +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.RequestNotExecutedException; +import org.apache.hc.core5.http.config.CharCodingConfig; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2AsyncRequester; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.Result; +import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class HttpBinAsyncCompatTest { + + static final Timeout TIMEOUT = Timeout.ofSeconds(5); + + private final HttpHost target; + @RegisterExtension + private final H2AsyncRequesterResource clientResource; + + public HttpBinAsyncCompatTest(final HttpHost target) { + this.target = target; + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)); + } + + void configure(final Consumer customizer) { + clientResource.configure(customizer); + } + + H2AsyncRequester client() { + return clientResource.start(); + } + + @Test + void test_sequential_request_execution() throws Exception { + final HttpAsyncRequester client = client(); + final List> requestMessages = Arrays.asList( + new Message<>(new BasicHttpRequest(Method.GET, target, "/headers")), + new Message<>( + new BasicHttpRequest(Method.POST, target, "/anything"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), + new Message<>( + new BasicHttpRequest(Method.PUT, target, "/anything"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), + new Message<>(new BasicHttpRequest(Method.GET, target, "/drip")), + new Message<>(new BasicHttpRequest(Method.GET, target, "/bytes/20000")), + new Message<>(new BasicHttpRequest(Method.GET, target, "/delay/2")), + new Message<>( + new BasicHttpRequest(Method.POST, target, "/delay/2"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)), + new Message<>( + new BasicHttpRequest(Method.PUT, target, "/delay/2"), + new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)) + ); + + for (final Message message : requestMessages) { + final HttpRequest request = message.getHead(); + final AsyncEntityProducer entityProducer = message.getBody(); + final Future> messageFuture = client.execute( + new BasicRequestProducer(request, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() + .setCharset(StandardCharsets.US_ASCII) + .setMalformedInputAction(CodingErrorAction.IGNORE) + .setUnmappableInputAction(CodingErrorAction.REPLACE) + .build())), + TIMEOUT, + null); + final Message response = messageFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertEquals(HttpStatus.SC_OK, response.getHead().getCode()); + } + } + + @Test + void test_pipelined_request_execution() throws Exception { + final HttpAsyncRequester client = client(); + final Future connectFuture = client.connect(target, TIMEOUT); + final AsyncClientEndpoint streamEndpoint = connectFuture.get(); + try { + final int n = 20; + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < n; i++) { + + final HttpRequest request; + final AsyncEntityProducer entityProducer; + if (i % 2 == 0) { + request = new BasicHttpRequest(Method.GET, target, "/headers"); + entityProducer = null; + } else { + request = new BasicHttpRequest(Method.POST, target, "/anything"); + entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN); + } + + streamEndpoint.execute( + new BasicRequestProducer(request, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom() + .setCharset(StandardCharsets.US_ASCII) + .setMalformedInputAction(CodingErrorAction.IGNORE) + .setUnmappableInputAction(CodingErrorAction.REPLACE) + .build())), + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + request, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(request, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + } else { + Assertions.fail(result.exception); + } + } + } finally { + streamEndpoint.releaseAndDiscard(); + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/SocksHttpBinAsyncCompatTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/SocksHttpBinAsyncCompatTest.java new file mode 100644 index 0000000000..8bf56415ac --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/nio/SocksHttpBinAsyncCompatTest.java @@ -0,0 +1,49 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.compatibility.nio; + +import java.net.SocketAddress; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.reactor.IOReactorConfig; + +public abstract class SocksHttpBinAsyncCompatTest extends HttpBinAsyncCompatTest { + + public SocksHttpBinAsyncCompatTest(final HttpHost target, final SocketAddress socksProxy, final String socksUser, final String socksPassword) { + super(target); + configure(bootstrap -> bootstrap + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .setSocksProxyAddress(socksProxy) + .setSocksProxyUsername(socksUser) + .setSocksProxyPassword(socksPassword) + .build()) + ); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/SocksProxyResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/ExecutorResource.java similarity index 60% rename from httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/SocksProxyResource.java rename to httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/ExecutorResource.java index e8158f99ae..26a30dfdb1 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/SocksProxyResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/ExecutorResource.java @@ -27,42 +27,31 @@ package org.apache.hc.core5.testing.extension; -import org.apache.hc.core5.testing.SocksProxy; -import org.apache.hc.core5.util.TimeValue; -import org.junit.jupiter.api.Assertions; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class SocksProxyResource implements BeforeEachCallback, AfterEachCallback { +public class ExecutorResource implements AfterEachCallback { - private static final Logger LOG = LoggerFactory.getLogger(SocksProxyResource.class); + private final ExecutorService executorService; - private SocksProxy proxy; + public ExecutorResource(final ExecutorService executorService) { + this.executorService = executorService; + } - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up SOCKS proxy"); - proxy = new SocksProxy(); - proxy.start(); + public ExecutorResource(final int nThreads) { + this.executorService = Executors.newFixedThreadPool(nThreads); } - @Override - public void afterEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Shutting down SOCKS proxy"); - if (proxy != null) { - try { - proxy.shutdown(TimeValue.ofSeconds(5)); - } catch (final Exception ignore) { - } - } + public ExecutorService getExecutorService() { + return executorService; } - public SocksProxy proxy() { - Assertions.assertNotNull(proxy); - return proxy; + @Override + public void afterEach(final ExtensionContext extensionContext) throws Exception { + executorService.shutdownNow(); } } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ClassicTestResources.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ClassicTestResources.java index ad80ee7df4..c5a35710ce 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ClassicTestResources.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ClassicTestResources.java @@ -67,7 +67,7 @@ public void beforeEach(final ExtensionContext extensionContext) throws Exception LOG.debug("Starting up test client"); client = new ClassicTestClient( - scheme == URIScheme.HTTPS ? SSLTestContexts.createClientSSLContext() : null, + scheme == URIScheme.HTTPS ? SSLTestContexts.createClientSSLContext() : null, SocketConfig.custom() .setSoTimeout(socketTimeout) .build()); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ExecutorResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ExecutorResource.java new file mode 100644 index 0000000000..30330014d4 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/ExecutorResource.java @@ -0,0 +1,57 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.extension.classic; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class ExecutorResource implements AfterEachCallback { + + private final ExecutorService executorService; + + public ExecutorResource(final ExecutorService executorService) { + this.executorService = executorService; + } + + public ExecutorResource(final int nThreads) { + this.executorService = Executors.newFixedThreadPool(nThreads); + } + + public ExecutorService getExecutorService() { + return executorService; + } + + @Override + public void afterEach(final ExtensionContext extensionContext) throws Exception { + executorService.shutdownNow(); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpRequesterResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpRequesterResource.java index b5d6be5312..4858e460b6 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpRequesterResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpRequesterResource.java @@ -32,39 +32,30 @@ import org.apache.hc.core5.http.impl.bootstrap.HttpRequester; import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; import org.apache.hc.core5.testing.classic.LoggingHttp1StreamListener; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HttpRequesterResource implements BeforeEachCallback, AfterEachCallback { +public class HttpRequesterResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(HttpRequesterResource.class); - private final Consumer bootstrapCustomizer; - + private final RequesterBootstrap bootstrap; private HttpRequester requester; - public HttpRequesterResource(final Consumer bootstrapCustomizer) { - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test client"); - final RequesterBootstrap bootstrap = RequesterBootstrap.bootstrap() - .setSslContext(SSLTestContexts.createClientSSLContext()) + public HttpRequesterResource() { + this.bootstrap = RequesterBootstrap.bootstrap() .setMaxTotal(2) .setDefaultMaxPerRoute(2) .setStreamListener(LoggingHttp1StreamListener.INSTANCE) .setConnPoolListener(LoggingConnPoolListener.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - requester = bootstrap.create(); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -79,7 +70,10 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } public HttpRequester start() { - Assertions.assertNotNull(requester); + if (requester == null) { + LOG.debug("Starting up test client"); + requester = bootstrap.create(); + } return requester; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpServerResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpServerResource.java index ec961f1046..cf09b8917f 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpServerResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/classic/HttpServerResource.java @@ -30,44 +30,32 @@ import java.io.IOException; import java.util.function.Consumer; -import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.bootstrap.HttpServer; import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.classic.LoggingExceptionListener; import org.apache.hc.core5.testing.classic.LoggingHttp1StreamListener; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HttpServerResource implements BeforeEachCallback, AfterEachCallback { +public class HttpServerResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(HttpServerResource.class); - private final URIScheme scheme; - private final Consumer bootstrapCustomizer; + private final ServerBootstrap bootstrap; private HttpServer server; - public HttpServerResource(final URIScheme scheme, final Consumer bootstrapCustomizer) { - this.scheme = scheme; - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test server"); - - final ServerBootstrap bootstrap = ServerBootstrap.bootstrap() - .setSslContext(scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null) + public HttpServerResource() { + this.bootstrap = ServerBootstrap.bootstrap() .setExceptionListener(LoggingExceptionListener.INSTANCE) .setStreamListener(LoggingHttp1StreamListener.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - server = bootstrap.create(); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -82,8 +70,11 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } public HttpServer start() throws IOException { - Assertions.assertNotNull(server); - server.start(); + if (server == null) { + LOG.debug("Starting up test server"); + server = bootstrap.create(); + server.start(); + } return server; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncRequesterResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncRequesterResource.java index 6ed7cfc9f0..8815e04dcc 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncRequesterResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncRequesterResource.java @@ -29,49 +29,41 @@ import java.util.function.Consumer; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http2.impl.nio.bootstrap.H2AsyncRequester; import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; -import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.junit.jupiter.api.Assertions; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class H2AsyncRequesterResource implements BeforeEachCallback, AfterEachCallback { +public class H2AsyncRequesterResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(H2AsyncRequesterResource.class); - private final Consumer bootstrapCustomizer; + private final H2RequesterBootstrap bootstrap; + private H2AsyncRequester requester; - private HttpAsyncRequester requester; - - public H2AsyncRequesterResource(final Consumer bootstrapCustomizer) { - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test client"); - final H2RequesterBootstrap bootstrap = H2RequesterBootstrap.bootstrap() - .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) + public H2AsyncRequesterResource() { + this.bootstrap = H2RequesterBootstrap.bootstrap() .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT) .setStreamListener(LoggingH2StreamListener.INSTANCE) .setConnPoolListener(LoggingConnPoolListener.INSTANCE) .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) .setExceptionCallback(LoggingExceptionCallback.INSTANCE) - .setIOSessionListener(LoggingIOSessionListener.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - requester = bootstrap.create(); + .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -85,9 +77,12 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } } - public HttpAsyncRequester start() { - Assertions.assertNotNull(requester); - requester.start(); + public H2AsyncRequester start() { + if (requester == null) { + LOG.debug("Starting up test client"); + requester = bootstrap.create(); + requester.start(); + } return requester; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncServerResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncServerResource.java index 188eac814f..04f71d5b89 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncServerResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2AsyncServerResource.java @@ -27,50 +27,42 @@ package org.apache.hc.core5.testing.extension.nio; +import java.io.IOException; import java.util.function.Consumer; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap; -import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.junit.jupiter.api.Assertions; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class H2AsyncServerResource implements BeforeEachCallback, AfterEachCallback { +public class H2AsyncServerResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(H2AsyncServerResource.class); - private final Consumer bootstrapCustomizer; - + private final H2ServerBootstrap bootstrap; private HttpAsyncServer server; - public H2AsyncServerResource(final Consumer bootstrapCustomizer) { - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test server"); - - final H2ServerBootstrap bootstrap = H2ServerBootstrap.bootstrap() - .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) + public H2AsyncServerResource() { + this.bootstrap = H2ServerBootstrap.bootstrap() .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER) .setStreamListener(LoggingH2StreamListener.INSTANCE) .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) .setExceptionCallback(LoggingExceptionCallback.INSTANCE) - .setIOSessionListener(LoggingIOSessionListener.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - server = bootstrap.create(); + .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -84,9 +76,12 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } } - public HttpAsyncServer start() { - Assertions.assertNotNull(server); - server.start(); + public HttpAsyncServer start() throws IOException { + if (server == null) { + LOG.debug("Starting up test server"); + server = bootstrap.create(); + server.start(); + } return server; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2MultiplexingRequesterResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2MultiplexingRequesterResource.java index 1739ef893d..03bba48023 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2MultiplexingRequesterResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/H2MultiplexingRequesterResource.java @@ -31,43 +31,36 @@ import org.apache.hc.core5.http2.impl.nio.bootstrap.H2MultiplexingRequester; import org.apache.hc.core5.http2.impl.nio.bootstrap.H2MultiplexingRequesterBootstrap; -import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; import org.apache.hc.core5.testing.nio.LoggingH2StreamListener; import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.junit.jupiter.api.Assertions; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class H2MultiplexingRequesterResource implements BeforeEachCallback, AfterEachCallback { +public class H2MultiplexingRequesterResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(H2MultiplexingRequesterResource.class); - private final Consumer bootstrapCustomizer; + private final H2MultiplexingRequesterBootstrap bootstrap; private H2MultiplexingRequester requester; - public H2MultiplexingRequesterResource(final Consumer bootstrapCustomizer) { - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test client"); - final H2MultiplexingRequesterBootstrap bootstrap = H2MultiplexingRequesterBootstrap.bootstrap() - .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) + public H2MultiplexingRequesterResource() { + this.bootstrap = H2MultiplexingRequesterBootstrap.bootstrap() .setStreamListener(LoggingH2StreamListener.INSTANCE) .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) .setExceptionCallback(LoggingExceptionCallback.INSTANCE) - .setIOSessionListener(LoggingIOSessionListener.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - requester = bootstrap.create(); + .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -82,8 +75,11 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } public H2MultiplexingRequester start() { - Assertions.assertNotNull(requester); - requester.start(); + if (requester == null) { + LOG.debug("Starting up test client"); + requester = bootstrap.create(); + requester.start(); + } return requester; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncRequesterResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncRequesterResource.java index 18a5ac67f1..28f5219949 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncRequesterResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncRequesterResource.java @@ -31,45 +31,38 @@ import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.classic.LoggingConnPoolListener; import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.junit.jupiter.api.Assertions; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HttpAsyncRequesterResource implements BeforeEachCallback, AfterEachCallback { +public class HttpAsyncRequesterResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(HttpAsyncRequesterResource.class); - private final Consumer bootstrapCustomizer; + private final AsyncRequesterBootstrap bootstrap; private HttpAsyncRequester requester; - public HttpAsyncRequesterResource(final Consumer bootstrapCustomizer) { - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test client"); - final AsyncRequesterBootstrap bootstrap = AsyncRequesterBootstrap.bootstrap() - .setTlsStrategy(new BasicClientTlsStrategy(SSLTestContexts.createClientSSLContext())) + public HttpAsyncRequesterResource() { + this.bootstrap = AsyncRequesterBootstrap.bootstrap() .setMaxTotal(2) .setDefaultMaxPerRoute(2) .setIOSessionListener(LoggingIOSessionListener.INSTANCE) .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT) .setConnPoolListener(LoggingConnPoolListener.INSTANCE) - .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - requester = bootstrap.create(); + .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -84,8 +77,11 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } public HttpAsyncRequester start() { - Assertions.assertNotNull(requester); - requester.start(); + if (requester == null) { + LOG.debug("Starting up test client"); + requester = bootstrap.create(); + requester.start(); + } return requester; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncServerResource.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncServerResource.java index d7ee307e4d..15c97c300e 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncServerResource.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/extension/nio/HttpAsyncServerResource.java @@ -27,48 +27,41 @@ package org.apache.hc.core5.testing.extension.nio; +import java.io.IOException; import java.util.function.Consumer; import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; -import org.apache.hc.core5.http.nio.ssl.BasicServerTlsStrategy; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.nio.LoggingExceptionCallback; import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener; import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator; import org.apache.hc.core5.testing.nio.LoggingIOSessionListener; -import org.junit.jupiter.api.Assertions; +import org.apache.hc.core5.testing.nio.LoggingReactorMetricsListener; import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HttpAsyncServerResource implements BeforeEachCallback, AfterEachCallback { +public class HttpAsyncServerResource implements AfterEachCallback { private static final Logger LOG = LoggerFactory.getLogger(HttpAsyncServerResource.class); - private final Consumer bootstrapCustomizer; + private final AsyncServerBootstrap bootstrap; private HttpAsyncServer server; - public HttpAsyncServerResource(final Consumer bootstrapCustomizer) { - this.bootstrapCustomizer = bootstrapCustomizer; - } - - @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { - LOG.debug("Starting up test server"); - - final AsyncServerBootstrap bootstrap = AsyncServerBootstrap.bootstrap() - .setTlsStrategy(new BasicServerTlsStrategy(SSLTestContexts.createServerSSLContext())) + public HttpAsyncServerResource() { + this.bootstrap = AsyncServerBootstrap.bootstrap() .setStreamListener(LoggingHttp1StreamListener.INSTANCE_SERVER) .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) .setExceptionCallback(LoggingExceptionCallback.INSTANCE) - .setIOSessionListener(LoggingIOSessionListener.INSTANCE); - bootstrapCustomizer.accept(bootstrap); - server = bootstrap.create(); + .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE); + } + + public void configure(final Consumer customizer) { + customizer.accept(bootstrap); } @Override @@ -82,9 +75,12 @@ public void afterEach(final ExtensionContext extensionContext) throws Exception } } - public HttpAsyncServer start() { - Assertions.assertNotNull(server); - server.start(); + public HttpAsyncServer start() throws IOException { + if (server == null) { + LOG.debug("Starting up test server"); + server = bootstrap.create(); + server.start(); + } return server; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java index 2651a1876a..a4d33cf473 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java @@ -708,7 +708,7 @@ public Map execute( final Map tempResponseExpectations = new HashMap<>(responseExpectations); tempResponseExpectations.put(STATUS, 201); final Map response = super.execute(defaultURI, request, requestHandler, tempResponseExpectations); - Assertions.assertEquals(200, response.get(STATUS)); + Assertions.assertEquals(200, response.get(STATUS)); return response; } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java index ebdee6816b..6707b23972 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/AsyncServerBootstrapFilterTest.java @@ -55,8 +55,11 @@ import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.HttpAsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.HttpAsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -69,47 +72,55 @@ abstract class AsyncServerBootstrapFilterTest { private static final Timeout TIMEOUT = Timeout.ofMinutes(1); @RegisterExtension - private final HttpAsyncServerResource serverResource = new HttpAsyncServerResource(bootstrap -> bootstrap - .setIOReactorConfig( - IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setRequestRouter(RequestRouter.>builder() + private final HttpAsyncServerResource serverResource; + @RegisterExtension + private final HttpAsyncRequesterResource clientResource; + + public AsyncServerBootstrapFilterTest() { + this.serverResource = new HttpAsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) + .setIOReactorConfig( + IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setRequestRouter(RequestRouter.>builder() .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) - .addFilterLast("test-filter", (request, entityDetails, context, responseTrigger, chain) -> - chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() { - - @Override - public void sendInformation( - final HttpResponse response) throws HttpException, IOException { - responseTrigger.sendInformation(response); - } - - @Override - public void submitResponse( - final HttpResponse response, - final AsyncEntityProducer entityProducer) throws HttpException, IOException { - response.setHeader("X-Test-Filter", "active"); - responseTrigger.submitResponse(response, entityProducer); - } - - @Override - public void pushPromise( - final HttpRequest promise, - final AsyncPushProducer responseProducer) throws HttpException, IOException { - responseTrigger.pushPromise(promise, responseProducer); - } - - }))); - - @RegisterExtension - private final HttpAsyncRequesterResource clientResource = new HttpAsyncRequesterResource(bootstrap -> bootstrap - .setIOReactorConfig(IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build())); - + .addFilterLast("test-filter", (request, entityDetails, context, responseTrigger, chain) -> + chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() { + + @Override + public void sendInformation( + final HttpResponse response) throws HttpException, IOException { + responseTrigger.sendInformation(response); + } + + @Override + public void submitResponse( + final HttpResponse response, + final AsyncEntityProducer entityProducer) throws HttpException, IOException { + response.setHeader("X-Test-Filter", "active"); + responseTrigger.submitResponse(response, entityProducer); + } + + @Override + public void pushPromise( + final HttpRequest promise, + final AsyncPushProducer responseProducer) throws HttpException, IOException { + responseTrigger.pushPromise(promise, responseProducer); + } + + }))); + + this.clientResource = new HttpAsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build())); + } @Test void testFilters() throws Exception { diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncHttp1TransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncHttp1TransportTest.java new file mode 100644 index 0000000000..3842b1e72a --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncHttp1TransportTest.java @@ -0,0 +1,101 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import java.net.InetSocketAddress; +import java.util.Random; +import java.util.concurrent.Future; + +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncRequestProducer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncResponseConsumer; +import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +abstract class ClassicToAsyncHttp1TransportTest extends ClassicToAsyncTransportTest { + + public ClassicToAsyncHttp1TransportTest(final URIScheme scheme, final HttpVersion version) { + super(scheme, version); + } + + @ValueSource(ints = {0, 2048, 10240}) + @ParameterizedTest(name = "{displayName}; content length: {0}") + void test_request_handling_no_keep_alive(final int contentSize) throws Exception { + final HttpAsyncServer server = serverResource.start(); + registerHandler("/echo", () -> new EchoHandler(1024)); + + final Future future = server.listen(new InetSocketAddress(0), scheme); + final ListenerEndpoint listener = future.get(); + final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); + final HttpAsyncRequester requester = clientResource.start(); + + final HttpHost target = new HttpHost(scheme.id, "localhost", address.getPort()); + + final int n = 10; + + for (int i = 0; i < n; i++) { + final byte[] temp = new byte[contentSize]; + new Random(System.currentTimeMillis()).nextBytes(temp); + + final ClassicHttpRequest request = ClassicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo/") + .addHeader(HttpHeaders.CONNECTION, "Close") + .setEntity(new ByteArrayEntity(temp, ContentType.DEFAULT_BINARY)) + .build(); + + final ClassicToAsyncRequestProducer requestProducer = new ClassicToAsyncRequestProducer(request, TIMEOUT); + final ClassicToAsyncResponseConsumer responseConsumer = new ClassicToAsyncResponseConsumer(TIMEOUT); + + requester.execute(requestProducer, responseConsumer, TIMEOUT, null); + + requestProducer.blockWaiting().execute(); + + try (ClassicHttpResponse response = responseConsumer.blockWaiting()) { + Assertions.assertEquals(200, response.getCode()); + final byte[] bytes = EntityUtils.toByteArray(response.getEntity()); + Assertions.assertNotNull(bytes); + Assertions.assertArrayEquals(temp, bytes); + } + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncIntegrationTests.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncIntegrationTests.java new file mode 100644 index 0000000000..44a89ce7fa --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.URIScheme; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; + +class ClassicToAsyncIntegrationTests { + + @Nested + @DisplayName("Classic over async transport (HTTP/1.1)") + class ClassicOverAsyncTransport extends ClassicToAsyncHttp1TransportTest { + + public ClassicOverAsyncTransport() { + super(URIScheme.HTTP, HttpVersion.HTTP_1_1); + } + + } + + @Nested + @DisplayName("Classic over async transport (HTTP/1.1, TLS)") + class ClassicOverAsyncTransportTls extends ClassicToAsyncHttp1TransportTest { + + public ClassicOverAsyncTransportTls() { + super(URIScheme.HTTPS, HttpVersion.HTTP_1_1); + } + + } + + @Nested + @DisplayName("Classic over async transport (HTTP/2)") + class ClassicOverAsyncTransportH2 extends ClassicToAsyncTransportTest { + + public ClassicOverAsyncTransportH2() { + super(URIScheme.HTTP, HttpVersion.HTTP_2); + } + + } + + @Nested + @DisplayName("Classic over async transport (HTTP/2, TLS)") + class ClassicOverAsyncTransportH2Tls extends ClassicToAsyncTransportTest { + + public ClassicOverAsyncTransportH2Tls() { + super(URIScheme.HTTPS, HttpVersion.HTTP_2); + } + + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncTransportTest.java new file mode 100644 index 0000000000..e9c82c0c8a --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/ClassicToAsyncTransportTest.java @@ -0,0 +1,391 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Queue; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.RequestNotExecutedException; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.entity.EntityTemplate; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncRequestProducer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncResponseConsumer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncServerExchangeHandler; +import org.apache.hc.core5.http.support.BasicRequestBuilder; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.Result; +import org.apache.hc.core5.testing.SSLTestContexts; +import org.apache.hc.core5.testing.extension.classic.ExecutorResource; +import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; +import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; +import org.apache.hc.core5.util.Timeout; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +abstract class ClassicToAsyncTransportTest { + + static final Timeout TIMEOUT = Timeout.ofMinutes(1); + + final URIScheme scheme; + @RegisterExtension + final H2AsyncServerResource serverResource; + @RegisterExtension + final H2AsyncRequesterResource clientResource; + @RegisterExtension + final ExecutorResource executorResource; + + public ClassicToAsyncTransportTest(final URIScheme scheme, final HttpVersion version) { + this.scheme = scheme; + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setVersionPolicy(version.lessEquals(HttpVersion.HTTP_1_1) ? HttpVersionPolicy.FORCE_HTTP_1 : HttpVersionPolicy.FORCE_HTTP_2) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) + .setIOReactorConfig( + IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setRequestRouter(RequestRouter.>builder() + .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) + ); + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setVersionPolicy(version.lessEquals(HttpVersion.HTTP_1_1) ? HttpVersionPolicy.FORCE_HTTP_1 : HttpVersionPolicy.FORCE_HTTP_2) + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + ); + this.executorResource = new ExecutorResource(5); + } + + void registerHandler( + final String pathPattern, + final Supplier requestHandlerSupplier) { + serverResource.configure(bootstrap -> bootstrap + .setRequestRouter(RequestRouter.>builder() + .addRoute(RequestRouter.LOCAL_AUTHORITY, pathPattern, requestHandlerSupplier) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) + ); + } + + void registerHandler(final String pathPattern, final HttpRequestHandler requestHandler) { + registerHandler(pathPattern, () -> new ClassicToAsyncServerExchangeHandler( + executorResource.getExecutorService(), + requestHandler, + LoggingExceptionCallback.INSTANCE)); + } + + @Test + void test_request_execution() throws Exception { + final HttpAsyncServer server = serverResource.start(); + registerHandler("/echo", () -> new EchoHandler(1024)); + + final Future future = server.listen(new InetSocketAddress(0), scheme); + final ListenerEndpoint listener = future.get(); + final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); + final HttpAsyncRequester requester = clientResource.start(); + + final HttpHost target = new HttpHost(scheme.id, "localhost", address.getPort()); + + final int n = 10; + + for (int i = 0; i < n; i++) { + final ClassicHttpRequest request1 = ClassicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .setEntity(new EntityTemplate( + -1, + ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8), + null, + outputStream -> { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) { + for (int ii = 0; ii < 500; ii++) { + writer.write("0123456789abcdef"); + writer.newLine(); + } + } + })) + .build(); + final ClassicToAsyncRequestProducer requestProducer = new ClassicToAsyncRequestProducer(request1, 16, TIMEOUT); + final ClassicToAsyncResponseConsumer responseConsumer = new ClassicToAsyncResponseConsumer(16, TIMEOUT); + + requester.execute(requestProducer, responseConsumer, TIMEOUT, null); + + requestProducer.blockWaiting().execute(); + + try (ClassicHttpResponse response = responseConsumer.blockWaiting()) { + final HttpEntity entity = response.getEntity(); + final ContentType contentType = ContentType.parse(entity.getContentType()); + final Charset charset = ContentType.getCharset(contentType, StandardCharsets.UTF_8); + + try (final InputStream inputStream = entity.getContent()) { + final StringBuilder buffer = new StringBuilder(); + final byte[] tmp = new byte[16]; + int l; + while ((l = inputStream.read(tmp)) != -1) { + buffer.append(charset.decode(ByteBuffer.wrap(tmp, 0, l))); + } + final StringTokenizer t1 = new StringTokenizer(buffer.toString(), "\r\n"); + while (t1.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t1.nextToken()); + } + } + } + } + } + + @ParameterizedTest(name = "method {0}") + @ValueSource(strings = {"GET", "POST", "HEAD"}) + void test_request_handling(final String method) throws Exception { + registerHandler("/hello", (request, response, context) -> { + final HttpEntity requestEntity = request.getEntity(); + if (requestEntity != null) { + EntityUtils.consume(requestEntity); + } + final ContentType contentType = ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8); + final Charset charset = contentType.getCharset(); + final HttpEntity responseEntity = new EntityTemplate( + contentType, + outputStream -> { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset))) { + for (int i = 0; i < 500; i++) { + writer.write("0123456789abcdef\r\n"); + } + } + }); + response.setEntity(responseEntity); + }); + + final HttpAsyncServer server = serverResource.start(); + final Future future = server.listen(new InetSocketAddress(0), scheme); + final ListenerEndpoint listener = future.get(); + final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); + final HttpAsyncRequester requester = clientResource.start(); + + final HttpHost target = new HttpHost(scheme.id, "localhost", address.getPort()); + + final int n = 10; + + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + + for (int i = 0; i < n; i++) { + final BasicHttpRequest request1 = BasicRequestBuilder.create(method) + .setHttpHost(target) + .setPath("/hello") + .build(); + final AsyncEntityProducer entityProducer = Method.POST.isSame(method) ? + new MultiLineEntityProducer("xxxxxxxxxxxx", 250) : null; + + requester.execute( + new BasicRequestProducer(request1, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + TIMEOUT, + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + request1, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(request1, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + if (Method.HEAD.isSame(method)) { + Assertions.assertNull(result.content); + } else { + Assertions.assertNotNull(result.content); + final StringTokenizer t1 = new StringTokenizer(result.content, "\r\n"); + while (t1.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t1.nextToken()); + } + } + } else { + Assertions.fail(result.exception); + } + } + } + + @Test + void test_request_handling_full_streaming() throws Exception { + registerHandler("/echo", (request, response, context) -> { + final HttpEntity requestEntity = request.getEntity(); + final ContentType contentType = requestEntity != null ? ContentType.parseLenient(requestEntity.getContentType()) : ContentType.TEXT_PLAIN; + final Charset charset = contentType.getCharset(StandardCharsets.UTF_8); + final HttpEntity responseEntity; + if (requestEntity != null) { + responseEntity = new EntityTemplate( + contentType, + outputStream -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(requestEntity.getContent())); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset))) { + String line; + while ((line = reader.readLine()) != null) { + writer.write(line); + writer.newLine(); + } + } + }); + } else { + responseEntity = null; + } + response.setEntity(responseEntity); + }); + + final HttpAsyncServer server = serverResource.start(); + final Future future = server.listen(new InetSocketAddress(0), scheme); + final ListenerEndpoint listener = future.get(); + final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); + final HttpAsyncRequester requester = clientResource.start(); + + final HttpHost target = new HttpHost(scheme.id, "localhost", address.getPort()); + + final int n = 10; + + final CountDownLatch countDownLatch = new CountDownLatch(n); + final Queue> resultQueue = new ConcurrentLinkedQueue<>(); + + for (int i = 0; i < n; i++) { + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); + final AsyncEntityProducer entityProducer = new MultiLineEntityProducer("0123456789abcdef", 500); + + requester.execute( + new BasicRequestProducer(request1, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + TIMEOUT, + new FutureCallback>() { + + @Override + public void completed(final Message responseMessage) { + resultQueue.add(new Result<>( + request1, + responseMessage.getHead(), + responseMessage.getBody())); + countDownLatch.countDown(); + } + + @Override + public void failed(final Exception ex) { + resultQueue.add(new Result<>(request1, ex)); + countDownLatch.countDown(); + } + + @Override + public void cancelled() { + failed(new RequestNotExecutedException()); + } + + }); + } + + Assertions.assertTrue(countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()), "Request executions have not completed in time"); + for (final Result result : resultQueue) { + if (result.isOK()) { + Assertions.assertNotNull(result.response); + Assertions.assertEquals(HttpStatus.SC_OK, result.response.getCode(), "Response message returned non 200 status"); + Assertions.assertNotNull(result.content); + final StringTokenizer t1 = new StringTokenizer(result.content, "\r\n"); + while (t1.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t1.nextToken()); + } + } else { + Assertions.fail(result.exception); + } + } + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2AlpnTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2AlpnTest.java index 0036959655..39c39b5519 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2AlpnTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2AlpnTest.java @@ -73,7 +73,6 @@ abstract class H2AlpnTest { private final boolean strictALPN; private final boolean h2Allowed; - @RegisterExtension private final H2AsyncServerResource serverResource; @RegisterExtension @@ -87,7 +86,8 @@ public H2AlpnTest(final boolean strictALPN, final boolean h2Allowed) throws Exce new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext()) : new BasicServerTlsStrategy(SSLTestContexts.createServerSSLContext()); - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) @@ -101,7 +101,8 @@ public H2AlpnTest(final boolean strictALPN, final boolean h2Allowed) throws Exce final TlsStrategy clientTlsStrategy = new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext()); - this.clientResource = new H2MultiplexingRequesterResource(bootstrap -> bootstrap + this.clientResource = new H2MultiplexingRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ConnPoolTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ConnPoolTest.java index cf80289d34..e72e4b1f38 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ConnPoolTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ConnPoolTest.java @@ -43,10 +43,13 @@ import org.apache.hc.core5.http2.nio.command.PingCommand; import org.apache.hc.core5.http2.nio.pool.H2ConnPool; import org.apache.hc.core5.http2.nio.support.BasicPingHandler; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; import org.apache.hc.core5.testing.extension.nio.H2MultiplexingRequesterResource; import org.apache.hc.core5.util.Timeout; @@ -66,8 +69,10 @@ class H2ConnPoolTest { private final H2MultiplexingRequesterResource clientResource; public H2ConnPoolTest() throws Exception { - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) @@ -79,7 +84,9 @@ public H2ConnPoolTest() throws Exception { ); this.clientConnCount = new AtomicLong(); - this.clientResource = new H2MultiplexingRequesterResource(bootstrap -> bootstrap + this.clientResource = new H2MultiplexingRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportMultiplexingTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportMultiplexingTest.java index 28b14c5483..408ff8d7e7 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportMultiplexingTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportMultiplexingTest.java @@ -34,6 +34,8 @@ import java.util.Queue; import java.util.Random; import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.hc.core5.concurrent.Cancellable; import org.apache.hc.core5.concurrent.CountDownLatchFutureCallback; @@ -48,6 +50,7 @@ import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; import org.apache.hc.core5.http.impl.routing.RequestRouter; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; import org.apache.hc.core5.http.nio.support.BasicClientExchangeHandler; @@ -56,8 +59,11 @@ import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.impl.nio.bootstrap.H2MultiplexingRequester; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; import org.apache.hc.core5.testing.extension.nio.H2MultiplexingRequesterResource; import org.apache.hc.core5.util.TimeValue; @@ -78,8 +84,10 @@ abstract class H2CoreTransportMultiplexingTest { public H2CoreTransportMultiplexingTest(final URIScheme scheme) { this.scheme = scheme; - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) @@ -89,7 +97,9 @@ public H2CoreTransportMultiplexingTest(final URIScheme scheme) { .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) ); - this.clientResource = new H2MultiplexingRequesterResource(bootstrap -> bootstrap + this.clientResource = new H2MultiplexingRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) @@ -139,6 +149,27 @@ void testSequentialRequests() throws Exception { assertThat(body3, CoreMatchers.equalTo("some more stuff")); } + @Test + void testLargeRequest() throws Exception { + final HttpAsyncServer server = serverResource.start(); + final Future future = server.listen(new InetSocketAddress(0), scheme); + final ListenerEndpoint listener = future.get(); + final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); + final H2MultiplexingRequester requester = clientResource.start(); + + final HttpHost target = new HttpHost(scheme.id, "localhost", address.getPort()); + final String content = IntStream.range(0, 1000).mapToObj(i -> "a lot of stuff").collect(Collectors.joining(" ")); + final Future> resultFuture = requester.execute( + new BasicRequestProducer(Method.POST, target, "/a-lot-of-stuff", AsyncEntityProducers.create(content, ContentType.TEXT_PLAIN)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null); + final Message message = resultFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + assertThat(message, CoreMatchers.notNullValue()); + final HttpResponse response = message.getHead(); + assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK)); + final String body = message.getBody(); + assertThat(body, CoreMatchers.equalTo(content)); + } + @Test void testMultiplexedRequests() throws Exception { final HttpAsyncServer server = serverResource.start(); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportTest.java index 9079b9aef2..c683e95249 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2CoreTransportTest.java @@ -36,7 +36,10 @@ import org.apache.hc.core5.http.impl.routing.RequestRouter; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -52,20 +55,28 @@ abstract class H2CoreTransportTest extends HttpCoreTransportTest { private final H2AsyncRequesterResource clientResource; public H2CoreTransportTest(final URIScheme scheme) { + this(scheme, null); + } + + public H2CoreTransportTest(final URIScheme scheme, final String tlsProtocol) { super(scheme); - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext(tlsProtocol))) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) - .setRequestRouter(RequestRouter.>builder() + .setRequestRouter(RequestRouter.>builder() .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) ); - this.clientResource = new H2AsyncRequesterResource(bootstrap -> bootstrap + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext(tlsProtocol))) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2IntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2IntegrationTest.java index f635786f57..e825eae5a0 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2IntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2IntegrationTest.java @@ -29,41 +29,23 @@ import static org.hamcrest.MatcherAssert.assertThat; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.LinkedList; -import java.util.List; -import java.util.Map; import java.util.Queue; import java.util.StringTokenizer; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EndpointDetails; -import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HeaderElements; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHeaders; @@ -76,432 +58,85 @@ import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.message.BasicHttpRequest; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; -import org.apache.hc.core5.http.nio.AsyncResponseProducer; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; -import org.apache.hc.core5.http.nio.CapacityChannel; -import org.apache.hc.core5.http.nio.DataStreamChannel; -import org.apache.hc.core5.http.nio.ResponseChannel; import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; -import org.apache.hc.core5.http.nio.entity.DigestingEntityConsumer; -import org.apache.hc.core5.http.nio.entity.DigestingEntityProducer; import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler; -import org.apache.hc.core5.http.nio.support.AbstractServerExchangeHandler; import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; -import org.apache.hc.core5.http.nio.support.BasicAsyncServerExpectationDecorator; import org.apache.hc.core5.http.nio.support.BasicPushProducer; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http.nio.support.BasicResponseProducer; -import org.apache.hc.core5.http.nio.support.classic.AbstractClassicEntityConsumer; -import org.apache.hc.core5.http.nio.support.classic.AbstractClassicEntityProducer; -import org.apache.hc.core5.http.nio.support.classic.AbstractClassicServerExchangeHandler; -import org.apache.hc.core5.http.protocol.DefaultHttpProcessor; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.apache.hc.core5.http2.H2Error; import org.apache.hc.core5.http2.H2StreamResetException; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.nio.command.PingCommand; import org.apache.hc.core5.http2.nio.support.BasicPingHandler; -import org.apache.hc.core5.http2.protocol.H2RequestConnControl; -import org.apache.hc.core5.http2.protocol.H2RequestContent; -import org.apache.hc.core5.http2.protocol.H2RequestTargetHost; import org.apache.hc.core5.reactor.Command; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.testing.extension.nio.H2TestResources; -import org.apache.hc.core5.util.TextUtils; -import org.apache.hc.core5.util.Timeout; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -abstract class H2IntegrationTest { - - private static final Timeout TIMEOUT = Timeout.ofMinutes(1); - private static final Timeout LONG_TIMEOUT = Timeout.ofMinutes(2); - - private final URIScheme scheme; +abstract class H2IntegrationTest extends HttpIntegrationTest { @RegisterExtension private final H2TestResources resources; public H2IntegrationTest(final URIScheme scheme) { - this.scheme = scheme; + super(scheme); this.resources = new H2TestResources(scheme, TIMEOUT); } - @BeforeEach - void setup() { - resources.server().configure(H2Config.DEFAULT); - resources.client().configure(H2Config.DEFAULT); - } - - private URI createRequestURI(final InetSocketAddress serverEndpoint, final String path) { - try { - return new URI(scheme.id, null, "localhost", serverEndpoint.getPort(), path, null, null); - } catch (final URISyntaxException e) { - throw new IllegalStateException(); - } - } - - @Test - void testSimpleGet() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 10; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - final String entity = result.getBody(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - Assertions.assertEquals("Hi there", entity); - } - } - - @Test - void testSimpleHead() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.HEAD, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertNull(result.getBody()); - } - } - - @Test - void testLargeGet() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/", () -> new MultiLineResponseHandler("0123456789abcdef", 5000)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/"), null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - - final Future> future2 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer(512)), null); - - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t1.nextToken()); - } - - final Message result2 = future2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result2); - final HttpResponse response2 = result2.getHead(); - Assertions.assertNotNull(response2); - Assertions.assertEquals(200, response2.getCode()); - final String s2 = result2.getBody(); - Assertions.assertNotNull(s2); - final StringTokenizer t2 = new StringTokenizer(s2, "\r\n"); - while (t2.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t2.nextToken()); - } + @Override + protected HttpTestServer server() { + return resources.server(); } - @Test - void testBasicPost() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 10; i++) { - final HttpRequest request = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); - queue.add(streamEndpoint.execute( - new BasicRequestProducer(request, new StringAsyncEntityProducer("Hi there", ContentType.TEXT_PLAIN)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - Assertions.assertEquals("Hi back", entity1); - } + @Override + protected HttpTestClient client() { + return resources.client(); } - @Test - void testLargePost() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("*", () -> new EchoHandler(2048)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/echo"), - new MultiLineEntityProducer("0123456789abcdef", 5000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t1.nextToken()); - } + @BeforeEach + void setup() { + resources.server().configure(H2Config.DEFAULT); + resources.client().configure(H2Config.DEFAULT); } @Test + @Override void testSlowResponseConsumer() throws Exception { - final H2TestServer server = resources.server(); final H2TestClient client = resources.client(); - server.register("/", () -> new MultiLineResponseHandler("0123456789abcd", 3)); - final InetSocketAddress serverEndpoint = server.start(); - client.configure(H2Config.custom() .setInitialWindowSize(16) .build()); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/"), null), - new BasicResponseConsumer<>(new AbstractClassicEntityConsumer(16, Executors.newSingleThreadExecutor()) { - - @Override - protected String consumeData( - final ContentType contentType, final InputStream inputStream) throws IOException { - final Charset charset = ContentType.getCharset(contentType, StandardCharsets.US_ASCII); - - final StringBuilder buffer = new StringBuilder(); - try { - final byte[] tmp = new byte[16]; - int l; - while ((l = inputStream.read(tmp)) != -1) { - buffer.append(charset.decode(ByteBuffer.wrap(tmp, 0, l))); - Thread.sleep(500); - } - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } - return buffer.toString(); - } - }), - null); - - final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcd", t1.nextToken()); - } - } - - @Test - void testSlowRequestProducer() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("*", () -> new EchoHandler(2048)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new AbstractClassicEntityProducer(4096, ContentType.TEXT_PLAIN, Executors.newSingleThreadExecutor()) { - - @Override - protected void produceData(final ContentType contentType, final OutputStream outputStream) throws IOException { - final Charset charset = ContentType.getCharset(contentType, StandardCharsets.US_ASCII); - try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset))) { - for (int i = 0; i < 500; i++) { - if (i % 100 == 0) { - writer.flush(); - Thread.sleep(500); - } - writer.write("0123456789abcdef\r\n"); - } - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } - } - - }), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t1.nextToken()); - } + super.testSlowResponseConsumer(); } @Test void testSlowResponseProducer() throws Exception { - final H2TestServer server = resources.server(); final H2TestClient client = resources.client(); - server.register("*", () -> new AbstractClassicServerExchangeHandler(2048, Executors.newSingleThreadExecutor()) { - - @Override - protected void handle( - final HttpRequest request, - final InputStream requestStream, - final HttpResponse response, - final OutputStream responseStream, - final HttpContext context) throws IOException, HttpException { - - if (!"/hello".equals(request.getPath())) { - response.setCode(HttpStatus.SC_NOT_FOUND); - return; - } - if (!Method.POST.name().equalsIgnoreCase(request.getMethod())) { - response.setCode(HttpStatus.SC_NOT_IMPLEMENTED); - return; - } - if (requestStream == null) { - return; - } - final Header h1 = request.getFirstHeader(HttpHeaders.CONTENT_TYPE); - final ContentType contentType = h1 != null ? ContentType.parse(h1.getValue()) : null; - final Charset charset = ContentType.getCharset(contentType, StandardCharsets.US_ASCII); - response.setCode(HttpStatus.SC_OK); - response.setHeader(h1); - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(requestStream, charset)); - final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(responseStream, charset))) { - try { - String l; - int count = 0; - while ((l = reader.readLine()) != null) { - writer.write(l); - writer.write("\r\n"); - count++; - if (count % 500 == 0) { - Thread.sleep(500); - } - } - writer.flush(); - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } - } - } - }); - final InetSocketAddress serverEndpoint = server.start(); - client.configure(H2Config.custom() .setInitialWindowSize(512) .build()); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcd", 2000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcd", t1.nextToken()); - } + super.testSlowResponseProducer(); } @Test @@ -527,6 +162,8 @@ protected void handle( }); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.configure(H2Config.custom() .setPushEnabled(true) .build()); @@ -534,14 +171,18 @@ protected void handle( final BlockingQueue> pushMessageQueue = new LinkedBlockingDeque<>(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), + new BasicRequestProducer(request, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - (request, context) -> new AbstractAsyncPushHandler>(new BasicResponseConsumer<>(new StringAsyncEntityConsumer())) { + (r, c) -> new AbstractAsyncPushHandler>(new BasicResponseConsumer<>(new StringAsyncEntityConsumer())) { @Override protected void handleResponse( @@ -628,17 +269,23 @@ public void failed(final Exception cause) { }); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.configure(H2Config.custom() .setPushEnabled(true) .build()); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request1 = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), + new BasicRequestProducer(request1, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); Assertions.assertNotNull(result1); @@ -670,18 +317,22 @@ void testExcessOfConcurrentStreams() throws Exception { .build()); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.configure(H2Config.custom() .setMaxConcurrentStreams(20) .build()); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); final Queue>> queue = new LinkedList<>(); for (int i = 0; i < 2000; i++) { - final HttpRequest request1 = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/")); + final BasicHttpRequest request1 = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); final Future> future = streamEndpoint.execute( new BasicRequestProducer(request1, null), new BasicResponseConsumer<>(new DiscardingEntityConsumer<>()), null); @@ -698,219 +349,6 @@ void testExcessOfConcurrentStreams() throws Exception { } } - @Test - void testExpectationFailed() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("*", () -> new MessageExchangeHandler(new StringAsyncEntityConsumer()) { - - @Override - protected void handle( - final Message request, - final AsyncServerRequestHandler.ResponseTrigger responseTrigger, - final HttpContext context) throws IOException, HttpException { - responseTrigger.submitResponse(new BasicResponseProducer(HttpStatus.SC_OK, "All is well"), context); - - } - }); - server.configure(handler -> new BasicAsyncServerExpectationDecorator(handler) { - - @Override - protected AsyncResponseProducer verify(final HttpRequest request, final HttpContext context) throws IOException, HttpException { - final Header h = request.getFirstHeader("password"); - if (h != null && "secret".equals(h.getValue())) { - return null; - } else { - return new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); - } - } - }); - server.configure(handler -> new BasicAsyncServerExpectationDecorator(handler) { - - @Override - protected AsyncResponseProducer verify(final HttpRequest request, final HttpContext context) throws IOException, HttpException { - final Header h = request.getFirstHeader("password"); - if (h != null && "secret".equals(h.getValue())) { - return null; - } else { - return new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); - } - } - }); - final InetSocketAddress serverEndpoint = server.start(); - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - request1.addHeader("password", "secret"); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcdef", 5000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertNotNull("All is well", result1.getBody()); - - final HttpRequest request2 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - final Future> future2 = streamEndpoint.execute( - new BasicRequestProducer(request2, new MultiLineEntityProducer("0123456789abcdef", 5000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result2 = future2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result2); - final HttpResponse response2 = result2.getHead(); - Assertions.assertNotNull(response2); - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response2.getCode()); - Assertions.assertNotNull("You shall not pass", result2.getBody()); - } - - @Test - void testPrematureResponse() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("*", new Supplier() { - - @Override - public AsyncServerExchangeHandler get() { - return new AsyncServerExchangeHandler() { - - private final AtomicReference responseProducer = new AtomicReference<>(); - - @Override - public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { - capacityChannel.update(Integer.MAX_VALUE); - } - - @Override - public void consume(final ByteBuffer src) throws IOException { - } - - @Override - public void streamEnd(final List trailers) throws HttpException, IOException { - } - - @Override - public void handleRequest( - final HttpRequest request, - final EntityDetails entityDetails, - final ResponseChannel responseChannel, - final HttpContext context) throws HttpException, IOException { - final AsyncResponseProducer producer; - final Header h = request.getFirstHeader("password"); - if (h != null && "secret".equals(h.getValue())) { - producer = new BasicResponseProducer(HttpStatus.SC_OK, "All is well"); - } else { - producer = new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); - } - responseProducer.set(producer); - producer.sendResponse(responseChannel, context); - } - - @Override - public int available() { - final AsyncResponseProducer producer = this.responseProducer.get(); - return producer.available(); - } - - @Override - public void produce(final DataStreamChannel channel) throws IOException { - final AsyncResponseProducer producer = this.responseProducer.get(); - producer.produce(channel); - } - - @Override - public void failed(final Exception cause) { - } - - @Override - public void releaseResources() { - } - }; - } - - }); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcdef", 5000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); - Assertions.assertNotNull("You shall not pass", result1.getBody()); - } - - @Test - void testMessageWithTrailers() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/hello", () -> new AbstractServerExchangeHandler>() { - - @Override - protected AsyncRequestConsumer> supplyConsumer( - final HttpRequest request, - final EntityDetails entityDetails, - final HttpContext context) throws HttpException { - return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); - } - - @Override - protected void handle( - final Message requestMessage, - final AsyncServerRequestHandler.ResponseTrigger responseTrigger, - final HttpContext context) throws HttpException, IOException { - responseTrigger.submitResponse(new BasicResponseProducer( - HttpStatus.SC_OK, - new DigestingEntityProducer("MD5", - new StringAsyncEntityProducer("Hello back with some trailers"))), context); - } - }); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/hello")); - final DigestingEntityConsumer entityConsumer = new DigestingEntityConsumer<>("MD5", new StringAsyncEntityConsumer()); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, null), - new BasicResponseConsumer<>(entityConsumer), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertEquals("Hello back with some trailers", result1.getBody()); - - final List
trailers = entityConsumer.getTrailers(); - Assertions.assertNotNull(trailers); - Assertions.assertEquals(2, trailers.size()); - final Map map = new HashMap<>(); - for (final Header header: trailers) { - map.put(TextUtils.toLowerCase(header.getName()), header.getValue()); - } - final String digest = TextUtils.toHexString(entityConsumer.getDigest()); - Assertions.assertEquals("MD5", map.get("digest-algo")); - Assertions.assertEquals(digest, map.get("digest")); - } - @Test void testConnectionPing() throws Exception { final H2TestServer server = resources.server(); @@ -919,17 +357,22 @@ void testConnectionPing() throws Exception { server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); final int n = 10; final CountDownLatch latch = new CountDownLatch(n); final AtomicInteger count = new AtomicInteger(0); for (int i = 0; i < n; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), + new BasicRequestProducer(request, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); streamEndpoint.execute(new PingCommand(new BasicPingHandler(result -> { if (result) { @@ -951,13 +394,17 @@ void testRequestWithInvalidConnectionHeader() throws Exception { server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); final Future sessionFuture = client.requestSession(new HttpHost("localhost", serverEndpoint.getPort()), TIMEOUT, null); final IOSession session = sessionFuture.get(); try (final ClientSessionEndpoint streamEndpoint = new ClientSessionEndpoint(session)) { - - final HttpRequest request = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/hello")); + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); request.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE); final HttpCoreContext context = HttpCoreContext.create(); final Future> future = streamEndpoint.execute( @@ -973,73 +420,14 @@ void testRequestWithInvalidConnectionHeader() throws Exception { } } - @Test - void testHeaderTooLarge() throws Exception { - final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - server.configure(H2Config.custom() - .setMaxHeaderListSize(100) - .build()); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/hello")); - request1.setHeader("big-f-header", "1234567890123456789012345678901234567890123456789012345678901234567890" + - "1234567890123456789012345678901234567890"); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(431, response1.getCode()); - Assertions.assertEquals("Maximum header list size exceeded", result1.getBody()); - } - - @Test - void testHeaderTooLargePost() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"GET", "POST"}) + void testHeaderTooLarge(final String method) throws Exception { final H2TestServer server = resources.server(); - final H2TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); server.configure(H2Config.custom() .setMaxHeaderListSize(100) .build()); - final InetSocketAddress serverEndpoint = server.start(); - client.configure( - new DefaultHttpProcessor(H2RequestContent.INSTANCE, H2RequestTargetHost.INSTANCE, H2RequestConnControl.INSTANCE)); - client.start(); - - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); - request1.setHeader("big-f-header", "1234567890123456789012345678901234567890123456789012345678901234567890" + - "1234567890123456789012345678901234567890"); - - final byte[] b = new byte[2048]; - for (int i = 0; i < b.length; i++) { - b[i] = (byte) ('a' + i % 10); - } - - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, AsyncEntityProducers.create(b, ContentType.TEXT_PLAIN)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(431, response1.getCode()); - Assertions.assertEquals("Maximum header list size exceeded", result1.getBody()); + super.testHeaderTooLarge(method); } } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ProtocolNegotiationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ProtocolNegotiationTest.java index 8b2b1d2741..09d6ce3e1b 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ProtocolNegotiationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ProtocolNegotiationTest.java @@ -51,8 +51,11 @@ import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -70,8 +73,10 @@ class H2ProtocolNegotiationTest { private final H2AsyncRequesterResource clientResource; public H2ProtocolNegotiationTest() { - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) @@ -81,8 +86,10 @@ public H2ProtocolNegotiationTest() { .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) ); - this.clientResource = new H2AsyncRequesterResource(bootstrap -> bootstrap + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java index 7638d2013a..61d2a85227 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2ServerBootstrapFiltersTest.java @@ -56,8 +56,11 @@ import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -70,48 +73,57 @@ abstract class H2ServerBootstrapFiltersTest { private static final Timeout TIMEOUT = Timeout.ofMinutes(1); @RegisterExtension - private final H2AsyncServerResource serverResource = new H2AsyncServerResource(bootstrap -> bootstrap - .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) - .setIOReactorConfig( - IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setRequestRouter(RequestRouter.>builder() + private final H2AsyncServerResource serverResource; + @RegisterExtension + private final H2AsyncRequesterResource clientResource; + + H2ServerBootstrapFiltersTest() { + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) + .setIOReactorConfig( + IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build()) + .setRequestRouter(RequestRouter.>builder() .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) - .addFilterLast("test-filter", (request, entityDetails, context, responseTrigger, chain) -> - chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() { - - @Override - public void sendInformation( - final HttpResponse response) throws HttpException, IOException { - responseTrigger.sendInformation(response); - } - - @Override - public void submitResponse( - final HttpResponse response, - final AsyncEntityProducer entityProducer) throws HttpException, IOException { - response.setHeader("X-Test-Filter", "active"); - responseTrigger.submitResponse(response, entityProducer); - } - - @Override - public void pushPromise( - final HttpRequest promise, - final AsyncPushProducer responseProducer) throws HttpException, IOException { - responseTrigger.pushPromise(promise, responseProducer); - } - - }))); - - @RegisterExtension - private final H2AsyncRequesterResource clientResource = new H2AsyncRequesterResource(bootstrap -> bootstrap - .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) - .setIOReactorConfig(IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build())); + .addFilterLast("test-filter", (request, entityDetails, context, responseTrigger, chain) -> + chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() { + + @Override + public void sendInformation( + final HttpResponse response) throws HttpException, IOException { + responseTrigger.sendInformation(response); + } + + @Override + public void submitResponse( + final HttpResponse response, + final AsyncEntityProducer entityProducer) throws HttpException, IOException { + response.setHeader("X-Test-Filter", "active"); + responseTrigger.submitResponse(response, entityProducer); + } + + @Override + public void pushPromise( + final HttpRequest promise, + final AsyncPushProducer responseProducer) throws HttpException, IOException { + responseTrigger.pushPromise(promise, responseProducer); + } + + }))); + + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) + .setIOReactorConfig(IOReactorConfig.custom() + .setSoTimeout(TIMEOUT) + .build())); + } @Test void testSequentialRequests() throws Exception { diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2SocksProxyCoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2SocksProxyCoreTransportTest.java deleted file mode 100644 index 5c32461301..0000000000 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/H2SocksProxyCoreTransportTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.core5.testing.nio; - -import java.io.IOException; - -import org.apache.hc.core5.function.Supplier; -import org.apache.hc.core5.http.URIScheme; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; -import org.apache.hc.core5.http.impl.routing.RequestRouter; -import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; -import org.apache.hc.core5.http2.HttpVersionPolicy; -import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.testing.extension.SocksProxyResource; -import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; -import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; -import org.apache.hc.core5.util.Timeout; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.extension.RegisterExtension; - -abstract class H2SocksProxyCoreTransportTest extends HttpCoreTransportTest { - - private static final Timeout TIMEOUT = Timeout.ofMinutes(1); - - @RegisterExtension - @Order(-Integer.MAX_VALUE) - private final SocksProxyResource proxyResource; - @RegisterExtension - private final H2AsyncServerResource serverResource; - @RegisterExtension - private final H2AsyncRequesterResource clientResource; - - public H2SocksProxyCoreTransportTest(final URIScheme scheme) { - super(scheme); - this.proxyResource = new SocksProxyResource(); - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap - .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) - .setIOReactorConfig( - IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setRequestRouter(RequestRouter.>builder() - .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) - .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) - .build()) - ); - this.clientResource = new H2AsyncRequesterResource(bootstrap -> bootstrap - .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) - .setIOReactorConfig(IOReactorConfig.custom() - .setSocksProxyAddress(proxyResource.proxy().getProxyAddress()) - .setSoTimeout(TIMEOUT) - .build()) - ); - } - - @Override - HttpAsyncServer serverStart() throws IOException { - return serverResource.start(); - } - - @Override - HttpAsyncRequester clientStart() { - return clientResource.start(); - } - -} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1AuthenticationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1AuthenticationTest.java index 6385fc6989..3a77f018e5 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1AuthenticationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1AuthenticationTest.java @@ -59,9 +59,12 @@ import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.HttpAsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.HttpAsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -79,7 +82,9 @@ abstract class Http1AuthenticationTest { private final HttpAsyncRequesterResource clientResource; public Http1AuthenticationTest(final boolean respondImmediately) { - this.serverResource = new HttpAsyncServerResource(bootstrap -> bootstrap + this.serverResource = new HttpAsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) @@ -120,7 +125,9 @@ protected AsyncEntityProducer generateResponseContent(final HttpResponse unautho } }) ); - this.clientResource = new HttpAsyncRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpAsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1CoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1CoreTransportTest.java index cddbf326a4..e309d3bdd9 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1CoreTransportTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1CoreTransportTest.java @@ -57,8 +57,11 @@ import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.HttpAsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.HttpAsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -76,11 +79,19 @@ abstract class Http1CoreTransportTest extends HttpCoreTransportTest { private final HttpAsyncRequesterResource clientResource; public Http1CoreTransportTest(final URIScheme scheme) { + this(scheme, null); + } + + public Http1CoreTransportTest(final URIScheme scheme, final String tlsProtocol) { super(scheme); - this.serverResource = new HttpAsyncServerResource(bootstrap -> bootstrap + this.serverResource = new HttpAsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext(tlsProtocol))) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) + .setTcpKeepIdle(5) + .setTcpKeepInterval(3) .build()) .setRequestRouter(RequestRouter.>builder() .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) @@ -114,7 +125,9 @@ public void pushPromise( })) ); - this.clientResource = new HttpAsyncRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpAsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext(tlsProtocol))) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1IntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1IntegrationTest.java index 9c8b80ba33..49bf019f64 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1IntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1IntegrationTest.java @@ -29,39 +29,21 @@ import static org.hamcrest.MatcherAssert.assertThat; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Random; -import java.util.StringTokenizer; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; +import org.apache.hc.core5.function.Callback; +import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.ContentLengthStrategy; import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HeaderElements; import org.apache.hc.core5.http.HttpException; @@ -73,7 +55,6 @@ import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.MalformedChunkCodingException; import org.apache.hc.core5.http.Message; -import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.config.CharCodingConfig; @@ -83,127 +64,90 @@ import org.apache.hc.core5.http.impl.Http1StreamListener; import org.apache.hc.core5.http.impl.HttpProcessors; import org.apache.hc.core5.http.impl.nio.AbstractContentEncoder; +import org.apache.hc.core5.http.impl.nio.AbstractMessageWriter; +import org.apache.hc.core5.http.impl.nio.DefaultHttpRequestFactory; +import org.apache.hc.core5.http.impl.nio.DefaultHttpRequestParser; import org.apache.hc.core5.http.impl.nio.ServerHttp1StreamDuplexer; import org.apache.hc.core5.http.message.BasicHttpRequest; import org.apache.hc.core5.http.message.BasicHttpResponse; -import org.apache.hc.core5.http.nio.AsyncEntityProducer; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; +import org.apache.hc.core5.http.message.BasicLineFormatter; +import org.apache.hc.core5.http.message.LineFormatter; +import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.apache.hc.core5.http.nio.AsyncResponseProducer; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; -import org.apache.hc.core5.http.nio.CapacityChannel; import org.apache.hc.core5.http.nio.ContentEncoder; -import org.apache.hc.core5.http.nio.DataStreamChannel; import org.apache.hc.core5.http.nio.HandlerFactory; import org.apache.hc.core5.http.nio.NHttpMessageParser; import org.apache.hc.core5.http.nio.NHttpMessageWriter; -import org.apache.hc.core5.http.nio.ResponseChannel; import org.apache.hc.core5.http.nio.SessionOutputBuffer; import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer; -import org.apache.hc.core5.http.nio.entity.DigestingEntityConsumer; -import org.apache.hc.core5.http.nio.entity.DigestingEntityProducer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; -import org.apache.hc.core5.http.nio.support.AbstractServerExchangeHandler; import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; import org.apache.hc.core5.http.nio.support.BasicAsyncServerExpectationDecorator; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http.nio.support.BasicResponseProducer; import org.apache.hc.core5.http.nio.support.ImmediateResponseExchangeHandler; -import org.apache.hc.core5.http.nio.support.classic.AbstractClassicEntityConsumer; -import org.apache.hc.core5.http.nio.support.classic.AbstractClassicEntityProducer; -import org.apache.hc.core5.http.nio.support.classic.AbstractClassicServerExchangeHandler; import org.apache.hc.core5.http.protocol.DefaultHttpProcessor; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpProcessor; -import org.apache.hc.core5.http.protocol.RequestConnControl; -import org.apache.hc.core5.http.protocol.RequestContent; -import org.apache.hc.core5.http.protocol.RequestTargetHost; import org.apache.hc.core5.http.protocol.RequestValidateHost; +import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.Http1TestResources; import org.apache.hc.core5.util.CharArrayBuffer; -import org.apache.hc.core5.util.TextUtils; import org.apache.hc.core5.util.Timeout; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -abstract class Http1IntegrationTest { - - private static final Timeout TIMEOUT = Timeout.ofMinutes(1); - private static final Timeout LONG_TIMEOUT = Timeout.ofMinutes(2); - - private final URIScheme scheme; - - private final ReentrantLock lock = new ReentrantLock(); +abstract class Http1IntegrationTest extends HttpIntegrationTest { @RegisterExtension private final Http1TestResources resources; public Http1IntegrationTest(final URIScheme scheme) { - this.scheme = scheme; + super(scheme); this.resources = new Http1TestResources(scheme, TIMEOUT); } - private URI createRequestURI(final InetSocketAddress serverEndpoint, final String path) { - try { - return new URI(scheme.id, null, "localhost", serverEndpoint.getPort(), path, null, null); - } catch (final URISyntaxException e) { - throw new IllegalStateException(); - } + @Override + protected HttpTestServer server() { + return resources.server(); } - @Test - void testSimpleGet() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertEquals("Hi there", entity1); - } + @Override + protected HttpTestClient client() { + return resources.client(); } @Test - void testSimpleGetConnectionClose() throws Exception { + void testGetConnectionClose() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final URI requestURI = createRequestURI(serverEndpoint, "/hello"); - for (int i = 0; i < 5; i++) { - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + for (int i = 0; i < REQ_NUM; i++) { + final Future connectFuture = client.connect(target, TIMEOUT); try (final ClientSessionEndpoint streamEndpoint = connectFuture.get()) { final Future> future = streamEndpoint.execute( - AsyncRequestBuilder.get(requestURI) + AsyncRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") .addHeader(HttpHeaders.CONNECTION, "close") .build(), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -219,7 +163,7 @@ void testSimpleGetConnectionClose() throws Exception { } @Test - void testSimpleGetIdentityTransfer() throws Exception { + void testGetIdentityTransfer() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); @@ -227,16 +171,20 @@ void testSimpleGetIdentityTransfer() throws Exception { server.configure(new DefaultHttpProcessor(new RequestValidateHost())); final InetSocketAddress serverEndpoint = server.start(); - client.start(); + final HttpHost target = target(serverEndpoint); - final int reqNo = 5; + client.start(); - for (int i = 0; i < reqNo; i++) { + for (int i = 0; i < REQ_NUM; i++) { final Future connectFuture = client.connect("localhost", serverEndpoint.getPort(), TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), + new BasicRequestProducer(request, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -261,18 +209,20 @@ void testPostIdentityTransfer() throws Exception { server.configure(new DefaultHttpProcessor(new RequestValidateHost())); final InetSocketAddress serverEndpoint = server.start(); - client.start(); + final HttpHost target = target(serverEndpoint); - final int reqNo = 5; + client.start(); - for (int i = 0; i < reqNo; i++) { + for (int i = 0; i < REQ_NUM; i++) { final Future connectFuture = client.connect("localhost", serverEndpoint.getPort(), TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, - createRequestURI(serverEndpoint, "/hello"), - new MultiLineEntityProducer("Hello", 16 * i)), + new BasicRequestProducer(request, new MultiLineEntityProducer("Hello", 16 * i)), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -288,7 +238,7 @@ void testPostIdentityTransfer() throws Exception { } @Test - void testPostIdentityTransferOutOfSequenceResponse() throws Exception { + void testPostIdentityTransferOutOfSequenceResponseNotOK() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); @@ -296,18 +246,20 @@ void testPostIdentityTransferOutOfSequenceResponse() throws Exception { server.configure(new DefaultHttpProcessor(new RequestValidateHost())); final InetSocketAddress serverEndpoint = server.start(); - client.start(); + final HttpHost target = target(serverEndpoint); - final int reqNo = 5; + client.start(); - for (int i = 0; i < reqNo; i++) { + for (int i = 0; i < REQ_NUM; i++) { final Future connectFuture = client.connect("localhost", serverEndpoint.getPort(), TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, - createRequestURI(serverEndpoint, "/hello"), - new MultiLineEntityProducer("Hello", 16 * i)), + new BasicRequestProducer(request, new MultiLineEntityProducer("Hello", 16 * i)), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -323,192 +275,24 @@ void testPostIdentityTransferOutOfSequenceResponse() throws Exception { } @Test - void testSimpleGetsPipelined() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 5; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - final String entity = result.getBody(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - Assertions.assertEquals("Hi there", entity); - } - } - - @Test - void testLargeGet() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/", () -> new MultiLineResponseHandler("0123456789abcdef", 5000)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t1.nextToken()); - } - - final Future> future2 = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer(512)), null); - - final Message result2 = future2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result2); - final HttpResponse response2 = result2.getHead(); - Assertions.assertNotNull(response2); - Assertions.assertEquals(200, response2.getCode()); - final String s2 = result2.getBody(); - Assertions.assertNotNull(s2); - final StringTokenizer t2 = new StringTokenizer(s2, "\r\n"); - while (t2.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t2.nextToken()); - } - } - - @Test - void testLargeGetsPipelined() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/", () -> new MultiLineResponseHandler("0123456789abcdef", 2000)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 5; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - final String entity = result.getBody(); - Assertions.assertNotNull(entity); - final StringTokenizer t = new StringTokenizer(entity, "\r\n"); - while (t.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t.nextToken()); - } - } - } - - @Test - void testBasicPost() throws Exception { + void testHTTP10Post() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); final InetSocketAddress serverEndpoint = server.start(); - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello"), - AsyncEntityProducers.create("Hi there")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertEquals("Hi back", entity1); - } - } - - @Test - void testBasicPostPipelined() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); - final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 5; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello"), - AsyncEntityProducers.create("Hi there")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - final String entity = result.getBody(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - Assertions.assertEquals("Hi back", entity); - } - } - - @Test - void testHttp10Post() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final HttpRequest request = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); request.setVersion(HttpVersion.HTTP_1_0); final Future> future = streamEndpoint.execute( new BasicRequestProducer(request, AsyncEntityProducers.create("Hi there")), @@ -531,12 +315,16 @@ void testHTTP11FeaturesDisabledWithHTTP10Requests() throws Exception { server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - final HttpRequest request = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); request.setVersion(HttpVersion.HTTP_1_0); final Future> future = streamEndpoint.execute( new BasicRequestProducer(request, new BasicAsyncEntityProducer(new byte[] {'a', 'b', 'c'}, null, true)), @@ -546,180 +334,24 @@ void testHTTP11FeaturesDisabledWithHTTP10Requests() throws Exception { } @Test - void testNoEntityPost() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final HttpRequest request = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(request, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertEquals("Hi back", entity1); - } - } - - @Test - void testLargePost() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("*", () -> new EchoHandler(2048)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/echo"), - new MultiLineEntityProducer("0123456789abcdef", 5000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - final String entity = result.getBody(); - Assertions.assertNotNull(entity); - final StringTokenizer t = new StringTokenizer(entity, "\r\n"); - while (t.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t.nextToken()); - } - } - } - - @Test - void testPostsPipelinedLargeResponse() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/", () -> new MultiLineResponseHandler("0123456789abcdef", 2000)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 2; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/"), - AsyncEntityProducers.create("Hi there")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - final String entity = result.getBody(); - Assertions.assertNotNull(entity); - final StringTokenizer t = new StringTokenizer(entity, "\r\n"); - while (t.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t.nextToken()); - } - } - } - - - @Test - void testLargePostsPipelined() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("*", () -> new EchoHandler(2048)); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 5; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/echo"), - new MultiLineEntityProducer("0123456789abcdef", 5000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - final String entity = result.getBody(); - Assertions.assertNotNull(entity); - final StringTokenizer t = new StringTokenizer(entity, "\r\n"); - while (t.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t.nextToken()); - } - } - } - - @Test - void testSimpleHead() throws Exception { + void testHeadConnectionClose() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); final InetSocketAddress serverEndpoint = server.start(); - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 5; i++) { - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.HEAD, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertNull(result.getBody()); - } - } - - @Test - void testSimpleHeadConnectionClose() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); client.start(); - final URI requestURI = createRequestURI(serverEndpoint, "/hello"); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < REQ_NUM; i++) { final Future connectFuture = client.connect( "localhost", serverEndpoint.getPort(), TIMEOUT); try (final ClientSessionEndpoint streamEndpoint = connectFuture.get()) { final Future> future = streamEndpoint.execute( - AsyncRequestBuilder.head(requestURI) + AsyncRequestBuilder.head() + .setHttpHost(target) + .setPath("/hello") .addHeader(HttpHeaders.CONNECTION, "close") .build(), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -734,36 +366,7 @@ void testSimpleHeadConnectionClose() throws Exception { } @Test - void testHeadPipelined() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 5; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.HEAD, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertNull(result.getBody()); - } - } - - @Test + @Override void testExpectationFailed() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); @@ -793,14 +396,19 @@ protected AsyncResponseProducer verify(final HttpRequest request, final HttpCont }); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); final Future sessionFuture = client.requestSession( new HttpHost("localhost", serverEndpoint.getPort()), TIMEOUT, null); final IOSession ioSession = sessionFuture.get(); try (final ClientSessionEndpoint streamEndpoint = new ClientSessionEndpoint(ioSession)) { - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - request1.addHeader("password", "secret"); + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .addHeader("password", "secret") + .build(); final Future> future1 = streamEndpoint.execute( new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcdef", 1000)), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -813,7 +421,10 @@ protected AsyncResponseProducer verify(final HttpRequest request, final HttpCont Assertions.assertTrue(ioSession.isOpen()); - final HttpRequest request2 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); + final BasicHttpRequest request2 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); final Future> future2 = streamEndpoint.execute( new BasicRequestProducer(request2, new MultiLineEntityProducer("0123456789abcdef", 5000)), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -826,8 +437,11 @@ protected AsyncResponseProducer verify(final HttpRequest request, final HttpCont Assertions.assertTrue(ioSession.isOpen()); - final HttpRequest request3 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - request3.addHeader("password", "secret"); + final BasicHttpRequest request3 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .addHeader("password", "secret") + .build(); final Future> future3 = streamEndpoint.execute( new BasicRequestProducer(request3, new MultiLineEntityProducer("0123456789abcdef", 1000)), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -840,7 +454,10 @@ protected AsyncResponseProducer verify(final HttpRequest request, final HttpCont Assertions.assertTrue(ioSession.isOpen()); - final HttpRequest request4 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); + final BasicHttpRequest request4 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); final Future> future4 = streamEndpoint.execute( new BasicRequestProducer(request4, AsyncEntityProducers.create("blah")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -887,13 +504,18 @@ protected AsyncResponseProducer verify(final HttpRequest request, final HttpCont }); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); final Future sessionFuture = client.requestSession( new HttpHost("localhost", serverEndpoint.getPort()), TIMEOUT, null); final IOSession ioSession = sessionFuture.get(); try (final ClientSessionEndpoint streamEndpoint = new ClientSessionEndpoint(ioSession)) { - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); final Future> future1 = streamEndpoint.execute( new BasicRequestProducer(request1, new MultiBinEntityProducer( new byte[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}, @@ -901,407 +523,77 @@ protected AsyncResponseProducer verify(final HttpRequest request, final HttpCont ContentType.TEXT_PLAIN)), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); - Assertions.assertNotNull("You shall not pass", result1.getBody()); - - Assertions.assertFalse(streamEndpoint.isOpen()); - } - } - - @Test - void testDelayedExpectationVerification() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("*", () -> new AsyncServerExchangeHandler() { - - private final Random random = new Random(System.currentTimeMillis()); - private final AsyncEntityProducer entityProducer = AsyncEntityProducers.create( - "All is well"); - - @Override - public void handleRequest( - final HttpRequest request, - final EntityDetails entityDetails, - final ResponseChannel responseChannel, - final HttpContext context) throws HttpException, IOException { - - Executors.newSingleThreadExecutor().execute(() -> { - try { - if (entityDetails != null) { - final Header h = request.getFirstHeader(HttpHeaders.EXPECT); - if (h != null && HeaderElements.CONTINUE.equalsIgnoreCase(h.getValue())) { - Thread.sleep(random.nextInt(1000)); - responseChannel.sendInformation(new BasicHttpResponse(HttpStatus.SC_CONTINUE), context); - } - final HttpResponse response = new BasicHttpResponse(200); - lock.lock(); - try { - responseChannel.sendResponse(response, entityProducer, context); - } finally { - lock.unlock(); - } - } - } catch (final Exception ignore) { - // ignore - } - }); - - } - - @Override - public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { - capacityChannel.update(Integer.MAX_VALUE); - } - - @Override - public void consume(final ByteBuffer src) throws IOException { - } - - @Override - public void streamEnd(final List trailers) throws HttpException, IOException { - } - - @Override - public int available() { - lock.lock(); - try { - return entityProducer.available(); - } finally { - lock.unlock(); - } - } - - @Override - public void produce(final DataStreamChannel channel) throws IOException { - lock.lock(); - try { - entityProducer.produce(channel); - } finally { - lock.unlock(); - } - } - - @Override - public void failed(final Exception cause) { - } - - @Override - public void releaseResources() { - } - - }); - final InetSocketAddress serverEndpoint = server.start(); - - client.configure(Http1Config.custom() - .setWaitForContinueTimeout(Timeout.ofMilliseconds(100)) - .build()); - client.start(); - - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Queue>> queue = new LinkedList<>(); - for (int i = 0; i < 5; i++) { - queue.add(streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/"), - AsyncEntityProducers.create("Some important message")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); - } - while (!queue.isEmpty()) { - final Future> future = queue.remove(); - final Message result = future.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response = result.getHead(); - Assertions.assertNotNull(response); - Assertions.assertEquals(200, response.getCode()); - Assertions.assertNotNull("All is well", result.getBody()); - } - } - - @Test - void testPrematureResponse() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("*", () -> new AsyncServerExchangeHandler() { - - private final AtomicReference responseProducer = new AtomicReference<>(); - - @Override - public void handleRequest( - final HttpRequest request, - final EntityDetails entityDetails, - final ResponseChannel responseChannel, - final HttpContext context) throws HttpException, IOException { - final AsyncResponseProducer producer; - final Header h = request.getFirstHeader("password"); - if (h != null && "secret".equals(h.getValue())) { - producer = new BasicResponseProducer(HttpStatus.SC_OK, "All is well"); - } else { - producer = new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); - } - responseProducer.set(producer); - producer.sendResponse(responseChannel, context); - } - - @Override - public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { - capacityChannel.update(Integer.MAX_VALUE); - } - - @Override - public void consume(final ByteBuffer src) throws IOException { - } - - @Override - public void streamEnd(final List trailers) throws HttpException, IOException { - } - - @Override - public int available() { - final AsyncResponseProducer producer = responseProducer.get(); - return producer.available(); - } - - @Override - public void produce(final DataStreamChannel channel) throws IOException { - final AsyncResponseProducer producer = responseProducer.get(); - producer.produce(channel); - } - - @Override - public void failed(final Exception cause) { - } - - @Override - public void releaseResources() { - } - }); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - for (int i = 0; i < 3; i++) { - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcdef", 100000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); - Assertions.assertNotNull("You shall not pass", result1.getBody()); - - Assertions.assertTrue(streamEndpoint.isOpen()); - } - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new MultiBinEntityProducer( - new byte[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}, - 100000, - ContentType.TEXT_PLAIN)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); - Assertions.assertNotNull("You shall not pass", result1.getBody()); - } - - @Test - void testSlowResponseConsumer() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/", () -> new MultiLineResponseHandler("0123456789abcd", 100)); - final InetSocketAddress serverEndpoint = server.start(); - - client.configure(Http1Config.custom() - .setBufferSize(256) - .build()); - client.start(); - - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, null), - new BasicResponseConsumer<>(new AbstractClassicEntityConsumer(16, Executors.newSingleThreadExecutor()) { - - @Override - protected String consumeData( - final ContentType contentType, final InputStream inputStream) throws IOException { - final Charset charset = ContentType.getCharset(contentType, StandardCharsets.US_ASCII); - - final StringBuilder buffer = new StringBuilder(); - try { - final byte[] tmp = new byte[16]; - int l; - while ((l = inputStream.read(tmp)) != -1) { - buffer.append(charset.decode(ByteBuffer.wrap(tmp, 0, l))); - Thread.sleep(50); - } - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } - return buffer.toString(); - } - }), - null); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); + Assertions.assertEquals("You shall not pass", result1.getBody()); - final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcd", t1.nextToken()); + Assertions.assertFalse(streamEndpoint.isOpen()); } } @Test - void testSlowRequestProducer() throws Exception { + void testMissingExpectContinueAckClientContinues() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); - server.register("*", () -> new EchoHandler(2048)); + // Disable 100-continue handshake on the server side + server.configure(handler -> handler); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there back")); final InetSocketAddress serverEndpoint = server.start(); - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final HttpHost target = target(serverEndpoint); - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/echo")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new AbstractClassicEntityProducer(4096, ContentType.TEXT_PLAIN, Executors.newSingleThreadExecutor()) { + client.configure(Http1Config.custom() + .setWaitForContinueTimeout(Timeout.ofMilliseconds(100)) + .build()); + client.start(); - @Override - protected void produceData(final ContentType contentType, final OutputStream outputStream) throws IOException { - final Charset charset = ContentType.getCharset(contentType, StandardCharsets.US_ASCII); - try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset))) { - for (int i = 0; i < 500; i++) { - if (i % 100 == 0) { - writer.flush(); - Thread.sleep(500); - } - writer.write("0123456789abcdef\r\n"); - } - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } - } + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - }), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcdef", t1.nextToken()); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, AsyncEntityProducers.create("Hi there")), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertEquals("Hi there back", entity1); } } @Test - void testSlowResponseProducer() throws Exception { - final Http1TestServer server = resources.server(); + void testSlowResponseConsumer() throws Exception { final Http1TestClient client = resources.client(); - server.register("*", () -> new AbstractClassicServerExchangeHandler(2048, Executors.newSingleThreadExecutor()) { + client.configure(Http1Config.custom() + .setBufferSize(256) + .build()); + client.start(); - @Override - protected void handle( - final HttpRequest request, - final InputStream requestStream, - final HttpResponse response, - final OutputStream responseStream, - final HttpContext context) throws IOException, HttpException { + super.testSlowResponseConsumer(); + } - if (!"/hello".equals(request.getPath())) { - response.setCode(HttpStatus.SC_NOT_FOUND); - return; - } - if (!Method.POST.name().equalsIgnoreCase(request.getMethod())) { - response.setCode(HttpStatus.SC_NOT_IMPLEMENTED); - return; - } - if (requestStream == null) { - return; - } - final Header h1 = request.getFirstHeader(HttpHeaders.CONTENT_TYPE); - final ContentType contentType = h1 != null ? ContentType.parse(h1.getValue()) : null; - final Charset charset = ContentType.getCharset(contentType, StandardCharsets.US_ASCII); - response.setCode(HttpStatus.SC_OK); - response.setHeader(h1); - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(requestStream, charset)); - final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(responseStream, charset))) { - try { - String l; - int count = 0; - while ((l = reader.readLine()) != null) { - writer.write(l); - writer.write("\r\n"); - count++; - if (count % 500 == 0) { - Thread.sleep(500); - } - } - writer.flush(); - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } - } - } - }); - final InetSocketAddress serverEndpoint = server.start(); + @Test + void testSlowResponseProducer() throws Exception { + final Http1TestClient client = resources.client(); client.configure(Http1Config.custom() .setBufferSize(256) .build()); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcd", 2000)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - final String s1 = result1.getBody(); - Assertions.assertNotNull(s1); - final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); - while (t1.hasMoreTokens()) { - Assertions.assertEquals("0123456789abcd", t1.nextToken()); - } + super.testSlowResponseProducer(); } @Test @@ -1312,24 +604,36 @@ void testPipelinedConnectionClose() throws Exception { server.register("/hello*", () -> new SingleLineResponseHandler("Hi back")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-1") + .build(); final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello-1"), - AsyncEntityProducers.create("Hi there")), + new BasicRequestProducer(request1, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final HttpRequest request2 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello-2")); - request2.addHeader(HttpHeaders.CONNECTION, "close"); + + final BasicHttpRequest request2 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-2") + .addHeader(HttpHeaders.CONNECTION, "close") + .build(); final Future> future2 = streamEndpoint.execute( new BasicRequestProducer(request2, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + + final BasicHttpRequest request3 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-3") + .build(); final Future> future3 = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello-3"), - AsyncEntityProducers.create("Hi there")), + new BasicRequestProducer(request3, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -1354,9 +658,12 @@ void testPipelinedConnectionClose() throws Exception { CoreMatchers.instanceOf(CancellationException.class), CoreMatchers.instanceOf(ExecutionException.class))); + final BasicHttpRequest request4 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-3") + .build(); final Future> future4 = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello-3"), - AsyncEntityProducers.create("Hi there")), + new BasicRequestProducer(request4, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Exception exception2 = Assertions.assertThrows(Exception.class, () -> future4.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); @@ -1373,24 +680,36 @@ void testPipelinedInvalidRequest() throws Exception { server.register("/hello*", () -> new SingleLineResponseHandler("Hi back")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-1") + .build(); final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello-1"), - AsyncEntityProducers.create("Hi there")), + new BasicRequestProducer(request1, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final HttpRequest request2 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello-2")); - request2.addHeader(HttpHeaders.HOST, "blah:blah"); + + final BasicHttpRequest request2 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-2") + .addHeader(HttpHeaders.HOST, "blah:blah") + .build(); final Future> future2 = streamEndpoint.execute( new BasicRequestProducer(request2, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + + final BasicHttpRequest request3 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello-3") + .build(); final Future> future3 = streamEndpoint.execute( - new BasicRequestProducer(Method.POST, createRequestURI(serverEndpoint, "/hello-3"), - AsyncEntityProducers.create("Hi there")), + new BasicRequestProducer(request3, AsyncEntityProducers.create("Hi there")), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -1407,7 +726,7 @@ void testPipelinedInvalidRequest() throws Exception { final String entity2 = result2.getBody(); Assertions.assertNotNull(response2); Assertions.assertEquals(400, response2.getCode()); - Assertions.assertTrue(entity2.length() > 0); + Assertions.assertFalse(entity2.isEmpty()); final Exception exception = Assertions.assertThrows(Exception.class, () -> @@ -1467,7 +786,7 @@ void testTruncatedChunk() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); - final InetSocketAddress serverEndpoint = server.start(new InternalServerHttp1EventHandlerFactory( + final InetSocketAddress serverEndpoint = server.startExecution(new InternalServerHttp1EventHandlerFactory( HttpProcessors.server(), (request, context) -> new MessageExchangeHandler(new StringAsyncEntityConsumer()) { @@ -1484,6 +803,8 @@ protected void handle( Http1Config.DEFAULT, CharCodingConfig.DEFAULT, DefaultConnectionReuseStrategy.INSTANCE, + null, + null, scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null, null, null) { @Override @@ -1498,13 +819,15 @@ protected ServerHttp1StreamDuplexer createServerHttp1StreamDuplexer( final NHttpMessageWriter outgoingMessageWriter, final ContentLengthStrategy incomingContentStrategy, final ContentLengthStrategy outgoingContentStrategy, - final Http1StreamListener streamListener) { + final Http1StreamListener streamListener, + final Callback exceptionCallback) { return new ServerHttp1StreamDuplexer(ioSession, httpProcessor, exchangeHandlerFactory, scheme.id, http1Config, connectionConfig, connectionReuseStrategy, incomingMessageParser, outgoingMessageWriter, incomingContentStrategy, outgoingContentStrategy, - streamListener) { + streamListener, + exceptionCallback) { @Override protected ContentEncoder createContentEncoder( @@ -1524,12 +847,17 @@ protected ContentEncoder createContentEncoder( }); + final HttpHost target = target(serverEndpoint); + client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - final AsyncRequestProducer requestProducer = new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")); + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final AsyncRequestProducer requestProducer = new BasicRequestProducer(request, null); final StringAsyncEntityConsumer entityConsumer = new StringAsyncEntityConsumer() { @Override @@ -1547,299 +875,96 @@ public void releaseResources() { Assertions.assertEquals("garbage", entityConsumer.generateContent()); } - @Test - void testExceptionInHandler() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there") { - - @Override - protected void handle( - final Message request, - final AsyncServerRequestHandler.ResponseTrigger responseTrigger, - final HttpContext context) throws IOException, HttpException { - throw new HttpException("Boom"); - } - }); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(500, response1.getCode()); - Assertions.assertEquals("Boom", entity1); - } - - @Test - void testNoServiceHandler() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"GET", "POST"}) + void testHeaderTooLarge(final String method) throws Exception { final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/ehh", () -> new SingleLineResponseHandler("Hi there")); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(404, response1.getCode()); - Assertions.assertEquals("Resource not found", entity1); + server.configure(Http1Config.custom() + .setMaxLineLength(100) + .build()); + super.testHeaderTooLarge(method); } @Test - void testResponseNoContent() throws Exception { + void testInvalidRequestMessage() throws Exception { + final Http1Config http1Config = Http1Config.DEFAULT; final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/hello", () -> new SingleLineResponseHandler("Hi there") { + server.configure(http1Config); + server.configure(() -> new DefaultHttpRequestParser(http1Config, DefaultHttpRequestFactory.INSTANCE) { @Override - protected void handle( - final Message request, - final AsyncServerRequestHandler.ResponseTrigger responseTrigger, - final HttpContext context) throws IOException, HttpException { - final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_NO_CONTENT); - responseTrigger.submitResponse(new BasicResponseProducer(response), context); + protected HttpRequest createMessage(final CharArrayBuffer buffer) throws HttpException { + throw new RuntimeException("Ka-boom"); } + }); - final InetSocketAddress serverEndpoint = server.start(); - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(204, response1.getCode()); - Assertions.assertNull(result.getBody()); - } + final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); - @Test - void testMessageWithTrailers() throws Exception { - final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); - server.register("/hello", () -> new AbstractServerExchangeHandler>() { - - @Override - protected AsyncRequestConsumer> supplyConsumer( - final HttpRequest request, - final EntityDetails entityDetails, - final HttpContext context) throws HttpException { - return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); - } - - @Override - protected void handle( - final Message requestMessage, - final AsyncServerRequestHandler.ResponseTrigger responseTrigger, - final HttpContext context) throws HttpException, IOException { - responseTrigger.submitResponse(new BasicResponseProducer( - HttpStatus.SC_OK, - new DigestingEntityProducer("MD5", - new StringAsyncEntityProducer("Hello back with some trailers"))), context); - } - }); - final InetSocketAddress serverEndpoint = server.start(); - client.start(); - final Future connectFuture = client.connect( "localhost", serverEndpoint.getPort(), TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - final HttpRequest request1 = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/hello")); - final DigestingEntityConsumer entityConsumer = new DigestingEntityConsumer<>("MD5", new StringAsyncEntityConsumer()); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, null), - new BasicResponseConsumer<>(entityConsumer), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(200, response1.getCode()); - Assertions.assertEquals("Hello back with some trailers", result1.getBody()); - - final List
trailers = entityConsumer.getTrailers(); - Assertions.assertNotNull(trailers); - Assertions.assertEquals(2, trailers.size()); - final Map map = new HashMap<>(); - for (final Header header: trailers) { - map.put(TextUtils.toLowerCase(header.getName()), header.getValue()); - } - final String digest = TextUtils.toHexString(entityConsumer.getDigest()); - Assertions.assertEquals("MD5", map.get("digest-algo")); - Assertions.assertEquals(digest, map.get("digest")); - } - - @Test - void testProtocolException() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); - - server.register("/boom", () -> new AsyncServerExchangeHandler() { - - private final StringAsyncEntityProducer entityProducer = new StringAsyncEntityProducer("Everyting is OK"); - - @Override - public void releaseResources() { - entityProducer.releaseResources(); - } - - @Override - public void handleRequest( - final HttpRequest request, - final EntityDetails entityDetails, - final ResponseChannel responseChannel, - final HttpContext context) throws HttpException, IOException { - final String requestUri = request.getRequestUri(); - if (requestUri.endsWith("boom")) { - throw new ProtocolException("Boom!!!"); - } - responseChannel.sendResponse(new BasicHttpResponse(200), entityProducer, context); - } - - @Override - public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { - capacityChannel.update(Integer.MAX_VALUE); - } - - @Override - public void consume(final ByteBuffer src) throws IOException { - } - - @Override - public void streamEnd(final List trailers) throws HttpException, IOException { - // empty - } - - @Override - public int available() { - return entityProducer.available(); - } - - @Override - public void produce(final DataStreamChannel channel) throws IOException { - entityProducer.produce(channel); - } - - @Override - public void failed(final Exception cause) { - releaseResources(); - } - - }); + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); - final InetSocketAddress serverEndpoint = server.start(); - - client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/boom")), + new BasicRequestProducer(request, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result); - final HttpResponse response1 = result.getHead(); - final String entity1 = result.getBody(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST, response1.getCode()); - Assertions.assertEquals("Boom!!!", entity1); + final ExecutionException executionException = Assertions.assertThrows(ExecutionException.class, () -> + future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); + Assertions.assertInstanceOf(ConnectionClosedException.class, executionException.getCause()); } @Test - void testHeaderTooLarge() throws Exception { + void testInvalidProtocolVersion() throws Exception { final Http1TestServer server = resources.server(); final Http1TestClient client = resources.client(); server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - server.configure(Http1Config.custom() - .setMaxLineLength(100) - .build()); final InetSocketAddress serverEndpoint = server.start(); - client.start(); - - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); - final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final HttpHost target = target(serverEndpoint); - final HttpRequest request1 = new BasicHttpRequest(Method.GET, createRequestURI(serverEndpoint, "/hello")); - request1.setHeader("big-f-header", "1234567890123456789012345678901234567890123456789012345678901234567890" + - "1234567890123456789012345678901234567890"); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(431, response1.getCode()); - Assertions.assertEquals("Maximum line length limit exceeded", result1.getBody()); - } + final LineFormatter lineFormatter = BasicLineFormatter.INSTANCE; + client.configure(() -> new AbstractMessageWriter(lineFormatter) { - @Test - void testHeaderTooLargePost() throws Exception { - final Http1TestServer server = resources.server(); - final Http1TestClient client = resources.client(); + @Override + protected void writeHeadLine(final HttpRequest message, final CharArrayBuffer lineBuf) throws IOException { + lineBuf.clear(); + lineFormatter.formatRequestLine(lineBuf, new RequestLine( + message.getMethod(), + message.getRequestUri(), + new HttpVersion(2, 1))); + } - server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); - server.configure(Http1Config.custom() - .setMaxLineLength(100) - .build()); - final InetSocketAddress serverEndpoint = server.start(); - client.configure( - new DefaultHttpProcessor(RequestContent.INSTANCE, RequestTargetHost.INSTANCE, RequestConnControl.INSTANCE)); + }); client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + "localhost", serverEndpoint.getPort(), TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); - final HttpRequest request1 = new BasicHttpRequest(Method.POST, createRequestURI(serverEndpoint, "/hello")); - request1.setHeader("big-f-header", "1234567890123456789012345678901234567890123456789012345678901234567890" + - "1234567890123456789012345678901234567890"); - - final byte[] b = new byte[2048]; - for (int i = 0; i < b.length; i++) { - b[i] = (byte) ('a' + i % 10); - } + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); - final Future> future1 = streamEndpoint.execute( - new BasicRequestProducer(request1, AsyncEntityProducers.create(b, ContentType.TEXT_PLAIN)), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); - final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertNotNull(result1); - final HttpResponse response1 = result1.getHead(); - Assertions.assertNotNull(response1); - Assertions.assertEquals(431, response1.getCode()); - Assertions.assertEquals("Maximum line length limit exceeded", result1.getBody()); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(505, response.getCode()); } } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1SocksProxyCoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1SocksProxyCoreTransportTest.java deleted file mode 100644 index 95c06534f9..0000000000 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/Http1SocksProxyCoreTransportTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.core5.testing.nio; - -import java.io.IOException; - -import org.apache.hc.core5.function.Supplier; -import org.apache.hc.core5.http.HeaderElements; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.URIScheme; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; -import org.apache.hc.core5.http.impl.bootstrap.StandardFilter; -import org.apache.hc.core5.http.impl.routing.RequestRouter; -import org.apache.hc.core5.http.nio.AsyncEntityProducer; -import org.apache.hc.core5.http.nio.AsyncFilterChain; -import org.apache.hc.core5.http.nio.AsyncPushProducer; -import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; -import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.testing.extension.SocksProxyResource; -import org.apache.hc.core5.testing.extension.nio.HttpAsyncRequesterResource; -import org.apache.hc.core5.testing.extension.nio.HttpAsyncServerResource; -import org.apache.hc.core5.util.Timeout; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.extension.RegisterExtension; - -abstract class Http1SocksProxyCoreTransportTest extends HttpCoreTransportTest { - - private static final Timeout TIMEOUT = Timeout.ofMinutes(1); - - @RegisterExtension - @Order(-Integer.MAX_VALUE) - private final SocksProxyResource proxyResource; - @RegisterExtension - private final HttpAsyncServerResource serverResource; - @RegisterExtension - private final HttpAsyncRequesterResource clientResource; - - public Http1SocksProxyCoreTransportTest(final URIScheme scheme) { - super(scheme); - this.proxyResource = new SocksProxyResource(); - this.serverResource = new HttpAsyncServerResource(bootstrap -> bootstrap - .setIOReactorConfig( - IOReactorConfig.custom() - .setSoTimeout(TIMEOUT) - .build()) - .setRequestRouter(RequestRouter.>builder() - .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) - .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) - .build()) - .addFilterBefore(StandardFilter.MAIN_HANDLER.name(), "no-keepalive", (request, entityDetails, context, responseTrigger, chain) -> - chain.proceed(request, entityDetails, context, new AsyncFilterChain.ResponseTrigger() { - - @Override - public void sendInformation( - final HttpResponse response) throws HttpException, IOException { - responseTrigger.sendInformation(response); - } - - @Override - public void submitResponse( - final HttpResponse response, - final AsyncEntityProducer entityProducer) throws HttpException, IOException { - if (request.getPath().startsWith("/no-keep-alive")) { - response.setHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE); - } - responseTrigger.submitResponse(response, entityProducer); - } - - @Override - public void pushPromise( - final HttpRequest promise, - final AsyncPushProducer responseProducer) throws HttpException, IOException { - responseTrigger.pushPromise(promise, responseProducer); - } - - })) - ); - this.clientResource = new HttpAsyncRequesterResource(bootstrap -> bootstrap - .setIOReactorConfig(IOReactorConfig.custom() - .setSocksProxyAddress(proxyResource.proxy().getProxyAddress()) - .setSoTimeout(TIMEOUT) - .build()) - ); - } - - @Override - HttpAsyncServer serverStart() throws IOException { - return serverResource.start(); - } - - @Override - HttpAsyncRequester clientStart() { - return clientResource.start(); - } - -} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpCoreTransportTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpCoreTransportTest.java index 8766d950bc..a9049b2e92 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpCoreTransportTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpCoreTransportTest.java @@ -34,6 +34,8 @@ import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpHost; @@ -47,6 +49,7 @@ import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; import org.apache.hc.core5.http.message.BasicHttpRequest; import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; @@ -62,7 +65,7 @@ abstract class HttpCoreTransportTest { final URIScheme scheme; - HttpCoreTransportTest(final URIScheme scheme) { + public HttpCoreTransportTest(final URIScheme scheme) { this.scheme = scheme; } @@ -113,6 +116,27 @@ void testSequentialRequests() throws Exception { assertThat(body3, CoreMatchers.equalTo("some more stuff")); } + @Test + void testLargeRequest() throws Exception { + final HttpAsyncServer server = serverStart(); + final Future future = server.listen(new InetSocketAddress(0), scheme); + final ListenerEndpoint listener = future.get(); + final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); + final HttpAsyncRequester requester = clientStart(); + + final HttpHost target = new HttpHost(scheme.id, "localhost", address.getPort()); + final String content = IntStream.range(0, 1000).mapToObj(i -> "a lot of stuff").collect(Collectors.joining(" ")); + final Future> resultFuture = requester.execute( + new BasicRequestProducer(Method.POST, target, "/a-lot-of-stuff", AsyncEntityProducers.create(content, ContentType.TEXT_PLAIN)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), TIMEOUT, null); + final Message message = resultFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + assertThat(message, CoreMatchers.notNullValue()); + final HttpResponse response = message.getHead(); + assertThat(response.getCode(), CoreMatchers.equalTo(HttpStatus.SC_OK)); + final String body = message.getBody(); + assertThat(body, CoreMatchers.equalTo(content)); + } + @Test void testSequentialRequestsNonPersistentConnection() throws Exception { final HttpAsyncServer server = serverStart(); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java new file mode 100644 index 0000000000..e018227bb0 --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java @@ -0,0 +1,1487 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.testing.nio; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Random; +import java.util.StringTokenizer; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HeaderElements; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.entity.EntityTemplate; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestConsumer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseProducer; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.CapacityChannel; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.RequestChannel; +import org.apache.hc.core5.http.nio.ResponseChannel; +import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; +import org.apache.hc.core5.http.nio.entity.DigestingEntityConsumer; +import org.apache.hc.core5.http.nio.entity.DigestingEntityProducer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.AbstractServerExchangeHandler; +import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; +import org.apache.hc.core5.http.nio.support.BasicAsyncServerExpectationDecorator; +import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.BasicResponseProducer; +import org.apache.hc.core5.http.nio.support.ImmediateResponseExchangeHandler; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncRequestProducer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncResponseConsumer; +import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncServerExchangeHandler; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.support.BasicRequestBuilder; +import org.apache.hc.core5.testing.extension.ExecutorResource; +import org.apache.hc.core5.util.TextUtils; +import org.apache.hc.core5.util.Timeout; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class HttpIntegrationTest { + + static final Timeout TIMEOUT = Timeout.ofMinutes(1); + static final Timeout LONG_TIMEOUT = Timeout.ofMinutes(2); + static final int REQ_NUM = 5; + + final URIScheme scheme; + @RegisterExtension + final ExecutorResource executorResource; + + public HttpIntegrationTest(final URIScheme scheme) { + this.scheme = scheme; + this.executorResource = new ExecutorResource(5); + } + + HttpHost target(final InetSocketAddress serverEndpoint) { + return new HttpHost(scheme.id, null, "localhost", serverEndpoint.getPort()); + } + + protected abstract HttpTestServer server(); + + protected abstract HttpTestClient client(); + + @Test + void testGet() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertEquals("Hi there", entity1); + } + } + + @Test + void testGetsPipelined() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + final String entity = result.getBody(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + Assertions.assertEquals("Hi there", entity); + } + } + + @Test + void testLargeGet() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/", () -> new MultiLineResponseHandler("0123456789abcdef", 5000)); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + final String s = result.getBody(); + Assertions.assertNotNull(s); + final StringTokenizer t = new StringTokenizer(s, "\r\n"); + while (t.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t.nextToken()); + } + } + } + + @Test + void testLargeGetsPipelined() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/", () -> new MultiLineResponseHandler("0123456789abcdef", 2000)); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + final String entity = result.getBody(); + Assertions.assertNotNull(entity); + final StringTokenizer t = new StringTokenizer(entity, "\r\n"); + while (t.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t.nextToken()); + } + } + } + + @Test + void testPost() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, AsyncEntityProducers.create("Hi there")), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertEquals("Hi back", entity1); + } + } + + @Test + void testPostPipelined() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, AsyncEntityProducers.create("Hi there")), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + final String entity = result.getBody(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + Assertions.assertEquals("Hi back", entity); + } + } + + @Test + void testLargePostsPipelined() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("*", () -> new EchoHandler(2048)); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, new MultiLineEntityProducer("0123456789abcdef", 5000)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + final String entity = result.getBody(); + Assertions.assertNotNull(entity); + final StringTokenizer t = new StringTokenizer(entity, "\r\n"); + while (t.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t.nextToken()); + } + } + } + + @Test + void testNoEntityPost() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi back")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertEquals("Hi back", entity1); + } + } + + @Test + void testPostOutOfSequenceResponseOK() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new ImmediateResponseExchangeHandler(200, "Welcome")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + + final Future connectFuture = client.connect("localhost", serverEndpoint.getPort(), TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, new MultiLineEntityProducer("Hello", 512 * i)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + final String entity = result.getBody(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + Assertions.assertEquals("Welcome", entity); + } + } + + @Test + void testHead() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.head() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertNull(result.getBody()); + } + } + + @Test + void testHeadPipelined() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.head() + .setHttpHost(target) + .setPath("/hello") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertNull(result.getBody()); + } + } + + @Test + void testSlowResponseConsumer() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/", () -> new MultiLineResponseHandler("0123456789abcd", 3)); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final ClassicHttpRequest request = ClassicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + final ClassicToAsyncResponseConsumer responseConsumer = new ClassicToAsyncResponseConsumer(16, TIMEOUT); + + streamEndpoint.execute( + new BasicRequestProducer(request, null), + responseConsumer, + null); + + final Random random = new Random(); + + try (ClassicHttpResponse response = responseConsumer.blockWaiting()) { + final HttpEntity entity = response.getEntity(); + final ContentType contentType = ContentType.parse(entity.getContentType()); + final Charset charset = ContentType.getCharset(contentType, StandardCharsets.UTF_8); + + try (final InputStream inputStream = entity.getContent()) { + final StringBuilder buffer = new StringBuilder(); + final byte[] tmp = new byte[16]; + int l; + while ((l = inputStream.read(tmp)) != -1) { + buffer.append(charset.decode(ByteBuffer.wrap(tmp, 0, l))); + Thread.sleep(100 + random.nextInt(400)); + } + final StringTokenizer t1 = new StringTokenizer(buffer.toString(), "\r\n"); + while (t1.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcd", t1.nextToken()); + } + } + } + } + + @Test + void testSlowRequestProducer() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("*", () -> new EchoHandler(2048)); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Random random = new Random(); + + final ClassicHttpRequest request1 = ClassicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .setEntity(new EntityTemplate( + -1, + ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8), + null, + outputStream -> { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) { + for (int i = 0; i < 500; i++) { + if (i % 100 == 0) { + writer.flush(); + Thread.sleep(100 + random.nextInt(400)); + } + writer.write("0123456789abcdef\r\n"); + } + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(ex.getMessage()); + } + })) + .build(); + final ClassicToAsyncRequestProducer requestProducer = new ClassicToAsyncRequestProducer(request1, 16, TIMEOUT); + + final Future> future1 = streamEndpoint.execute( + requestProducer, + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + requestProducer.blockWaiting().execute(); + + final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + final String s1 = result1.getBody(); + Assertions.assertNotNull(s1); + final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); + while (t1.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t1.nextToken()); + } + } + + @Test + void testSlowResponseProducer() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + final Random random = new Random(); + + final HttpRequestHandler requestHandler = (request, response, context) -> { + final HttpEntity requestEntity = request.getEntity(); + if (requestEntity != null) { + EntityUtils.consume(requestEntity); + } + final HttpEntity responseEntity = new EntityTemplate( + ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8), + outputStream -> { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) { + for (int i = 0; i < 500; i++) { + if (i % 100 == 0) { + writer.flush(); + Thread.sleep(100 + random.nextInt(400)); + } + writer.write("0123456789abcdef\r\n"); + } + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(ex.getMessage()); + } + }); + response.setEntity(responseEntity); + }; + + final RequestRouter requestRouter = RequestRouter.builder() + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .addRoute(RequestRouter.LOCAL_AUTHORITY, "/hello", requestHandler) + .build(); + + server.register("*", () -> new ClassicToAsyncServerExchangeHandler( + Executors.newSingleThreadExecutor(), + requestRouter, + LoggingExceptionCallback.INSTANCE)); + + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final BasicHttpRequest request1 = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future1 = streamEndpoint.execute( + new BasicRequestProducer(request1, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result1 = future1.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + final String s1 = result1.getBody(); + Assertions.assertNotNull(s1); + final StringTokenizer t1 = new StringTokenizer(s1, "\r\n"); + while (t1.hasMoreTokens()) { + Assertions.assertEquals("0123456789abcdef", t1.nextToken()); + } + } + + @Test + void testPrematureResponse() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("*", () -> new AsyncServerExchangeHandler() { + + private final AtomicReference responseProducer = new AtomicReference<>(); + + @Override + public void handleRequest( + final HttpRequest request, + final EntityDetails entityDetails, + final ResponseChannel responseChannel, + final HttpContext context) throws HttpException, IOException { + final AsyncResponseProducer producer; + final Header h = request.getFirstHeader("password"); + if (h != null && "secret".equals(h.getValue())) { + producer = new BasicResponseProducer(HttpStatus.SC_OK, "All is well"); + } else { + producer = new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); + } + responseProducer.set(producer); + producer.sendResponse(responseChannel, context); + } + + @Override + public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { + capacityChannel.update(Integer.MAX_VALUE); + } + + @Override + public void consume(final ByteBuffer src) throws IOException { + } + + @Override + public void streamEnd(final List trailers) throws HttpException, IOException { + } + + @Override + public int available() { + final AsyncResponseProducer producer = responseProducer.get(); + return producer.available(); + } + + @Override + public void produce(final DataStreamChannel channel) throws IOException { + final AsyncResponseProducer producer = responseProducer.get(); + producer.produce(channel); + } + + @Override + public void failed(final Exception cause) { + } + + @Override + public void releaseResources() { + } + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + for (int i = 0; i < 3; i++) { + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); + final Future> future1 = streamEndpoint.execute( + new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcdef", 100000)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); + Assertions.assertEquals("You shall not pass", result1.getBody()); + + Assertions.assertTrue(streamEndpoint.isOpen()); + } + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); + final Future> future1 = streamEndpoint.execute( + new BasicRequestProducer(request1, new MultiBinEntityProducer( + new byte[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}, + 100000, + ContentType.TEXT_PLAIN)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode()); + Assertions.assertEquals("You shall not pass", result1.getBody()); + } + + @Test + void testExpectationFailed() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("*", () -> new MessageExchangeHandler(new StringAsyncEntityConsumer()) { + + @Override + protected void handle( + final Message request, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws IOException, HttpException { + responseTrigger.submitResponse(new BasicResponseProducer(HttpStatus.SC_OK, "All is well"), context); + + } + }); + server.configure(handler -> new BasicAsyncServerExpectationDecorator(handler) { + + @Override + protected AsyncResponseProducer verify(final HttpRequest request, final HttpContext context) throws IOException, HttpException { + final Header h = request.getFirstHeader("password"); + if (h != null && "secret".equals(h.getValue())) { + return null; + } else { + return new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); + } + } + }); + server.configure(handler -> new BasicAsyncServerExpectationDecorator(handler) { + + @Override + protected AsyncResponseProducer verify(final HttpRequest request, final HttpContext context) throws IOException, HttpException { + final Header h = request.getFirstHeader("password"); + if (h != null && "secret".equals(h.getValue())) { + return null; + } else { + return new BasicResponseProducer(HttpStatus.SC_UNAUTHORIZED, "You shall not pass"); + } + } + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final BasicHttpRequest request1 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .addHeader("password", "secret") + .build(); + final Future> future1 = streamEndpoint.execute( + new BasicRequestProducer(request1, new MultiLineEntityProducer("0123456789abcdef", 5000)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertEquals("All is well", result1.getBody()); + + final BasicHttpRequest request2 = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/echo") + .build(); + final Future> future2 = streamEndpoint.execute( + new BasicRequestProducer(request2, new MultiLineEntityProducer("0123456789abcdef", 5000)), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result2 = future2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result2); + final HttpResponse response2 = result2.getHead(); + Assertions.assertNotNull(response2); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response2.getCode()); + Assertions.assertEquals("You shall not pass", result2.getBody()); + } + + @Test + void testDelayedExpectContinueAck() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + // Disable 100-continue handshake on the server side + server.configure(handler -> handler); + + server.register("*", () -> new AsyncServerExchangeHandler() { + + private final Random random = new Random(System.currentTimeMillis()); + private final AsyncEntityProducer entityProducer = AsyncEntityProducers.create( + "All is well"); + + @Override + public void handleRequest( + final HttpRequest request, + final EntityDetails entityDetails, + final ResponseChannel responseChannel, + final HttpContext context) throws HttpException, IOException { + + executorResource.getExecutorService().execute(() -> { + try { + if (entityDetails != null) { + final Header h = request.getFirstHeader(HttpHeaders.EXPECT); + if (h != null && HeaderElements.CONTINUE.equalsIgnoreCase(h.getValue())) { + Thread.sleep(random.nextInt(1000)); + responseChannel.sendInformation(new BasicHttpResponse(HttpStatus.SC_CONTINUE), context); + } + final HttpResponse response = new BasicHttpResponse(200); + responseChannel.sendResponse(response, entityProducer, context); + } + } catch (final Exception ignore) { + // ignore + } + }); + + } + + @Override + public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { + capacityChannel.update(Integer.MAX_VALUE); + } + + @Override + public void consume(final ByteBuffer src) throws IOException { + } + + @Override + public void streamEnd(final List trailers) throws HttpException, IOException { + } + + @Override + public int available() { + return entityProducer.available(); + } + + @Override + public void produce(final DataStreamChannel channel) throws IOException { + entityProducer.produce(channel); + } + + @Override + public void failed(final Exception cause) { + } + + @Override + public void releaseResources() { + } + + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, AsyncEntityProducers.create("Some important message")), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + Assertions.assertEquals("All is well", result.getBody()); + } + } + + @Test + void testExceptionInHandler() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there") { + + @Override + protected void handle( + final Message request, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws IOException, HttpException { + throw new HttpException("Boom"); + } + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(500, response1.getCode()); + Assertions.assertEquals("Boom", entity1); + } + + @Test + void testMessageWithTrailers() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new AbstractServerExchangeHandler>() { + + @Override + protected AsyncRequestConsumer> supplyConsumer( + final HttpRequest request, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); + } + + @Override + protected void handle( + final Message requestMessage, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, IOException { + responseTrigger.submitResponse(new BasicResponseProducer( + HttpStatus.SC_OK, + new DigestingEntityProducer("MD5", + new StringAsyncEntityProducer("Hello back with some trailers"))), context); + } + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final BasicHttpRequest request1 = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final DigestingEntityConsumer entityConsumer = new DigestingEntityConsumer<>("MD5", new StringAsyncEntityConsumer()); + final Future> future1 = streamEndpoint.execute( + new BasicRequestProducer(request1, null), + new BasicResponseConsumer<>(entityConsumer), null); + final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(200, response1.getCode()); + Assertions.assertEquals("Hello back with some trailers", result1.getBody()); + + final List
trailers = entityConsumer.getTrailers(); + Assertions.assertNotNull(trailers); + Assertions.assertEquals(2, trailers.size()); + final Map map = new HashMap<>(); + for (final Header header: trailers) { + map.put(TextUtils.toLowerCase(header.getName()), header.getValue()); + } + final String digest = TextUtils.toHexString(entityConsumer.getDigest()); + Assertions.assertEquals("MD5", map.get("digest-algo")); + Assertions.assertEquals(digest, map.get("digest")); + } + + @Test + void testNoServiceHandler() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/ehh", () -> new SingleLineResponseHandler("Hi there")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(404, response1.getCode()); + Assertions.assertEquals("Resource not found", entity1); + } + + @Test + void testResponseNoContent() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there") { + + @Override + protected void handle( + final Message request, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws IOException, HttpException { + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_NO_CONTENT); + responseTrigger.submitResponse(new BasicResponseProducer(response), context); + } + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(204, response1.getCode()); + Assertions.assertNull(result.getBody()); + } + + @Test + void testProtocolException() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/boom", () -> new AsyncServerExchangeHandler() { + + private final StringAsyncEntityProducer entityProducer = new StringAsyncEntityProducer("Everything is OK"); + + @Override + public void releaseResources() { + entityProducer.releaseResources(); + } + + @Override + public void handleRequest( + final HttpRequest request, + final EntityDetails entityDetails, + final ResponseChannel responseChannel, + final HttpContext context) throws HttpException, IOException { + final String requestUri = request.getRequestUri(); + if (requestUri.endsWith("boom")) { + throw new ProtocolException("Boom!!!"); + } + responseChannel.sendResponse(new BasicHttpResponse(200), entityProducer, context); + } + + @Override + public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { + capacityChannel.update(Integer.MAX_VALUE); + } + + @Override + public void consume(final ByteBuffer src) throws IOException { + } + + @Override + public void streamEnd(final List trailers) throws HttpException, IOException { + // empty + } + + @Override + public int available() { + return entityProducer.available(); + } + + @Override + public void produce(final DataStreamChannel channel) throws IOException { + entityProducer.produce(channel); + } + + @Override + public void failed(final Exception cause) { + releaseResources(); + } + + }); + + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/boom") + .build(); + final Future> future = streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response1 = result.getHead(); + final String entity1 = result.getBody(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST, response1.getCode()); + Assertions.assertEquals("Boom!!!", entity1); + } + + @Test @Disabled + void testDelayedRequestSubmission() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("All is well")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.post() + .setHttpHost(target) + .setPath("/hello") + .build(); + final AsyncEntityProducer entityProducer = AsyncEntityProducers.create("Some important message"); + queue.add(streamEndpoint.execute( + new AsyncRequestProducer() { + + private final Random random = new Random(System.currentTimeMillis()); + + @Override + public void sendRequest(final RequestChannel channel, final HttpContext context) throws HttpException, IOException { + executorResource.getExecutorService().execute(() -> { + try { + Thread.sleep(random.nextInt(200)); + channel.sendRequest(request, entityProducer, context); + } catch (final Exception ignore) { + // ignore + } + }); + } + + @Override + public boolean isRepeatable() { + return entityProducer.isRepeatable(); + } + + @Override + public int available() { + return entityProducer.available(); + } + + @Override + public void produce(final DataStreamChannel channel) throws IOException { + entityProducer.produce(channel); + } + + @Override + public void failed(final Exception cause) { + entityProducer.failed(cause); + } + + @Override + public void releaseResources() { + entityProducer.releaseResources(); + } + + }, + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + Assertions.assertEquals("All is well", result.getBody()); + } + } + + @Test + void testDelayedResponseSubmission() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new AbstractServerExchangeHandler>() { + + private final Random random = new Random(System.currentTimeMillis()); + + @Override + protected AsyncRequestConsumer> supplyConsumer( + final HttpRequest request, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); + } + + @Override + protected void handle( + final Message requestMessage, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, IOException { + executorResource.getExecutorService().execute(() -> { + try { + Thread.sleep(random.nextInt(200)); + responseTrigger.submitResponse(AsyncResponseBuilder.create(HttpStatus.SC_OK) + .setEntity(new MultiLineEntityProducer("All is well", 100)) + .build(), + context); + Thread.sleep(random.nextInt(200)); + } catch (final Exception ignore) { + // ignore + } + }); + + } + + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + + final Future connectFuture = client.connect( + "localhost", serverEndpoint.getPort(), TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + MatcherAssert.assertThat(result.getBody(), Matchers.startsWith("All is well")); + } + } + + @Test + void testDelayedResponseSubmissionNoResponseBody() throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new AbstractServerExchangeHandler>() { + + private final Random random = new Random(System.currentTimeMillis()); + + @Override + protected AsyncRequestConsumer> supplyConsumer( + final HttpRequest request, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); + } + + @Override + protected void handle( + final Message requestMessage, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, IOException { + executorResource.getExecutorService().execute(() -> { + try { + Thread.sleep(random.nextInt(200)); + responseTrigger.submitResponse(AsyncResponseBuilder.create(200) + .build(), + context); + Thread.sleep(random.nextInt(200)); + } catch (final Exception ignore) { + // ignore + } + }); + + } + + }); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + + final Future connectFuture = client.connect( + "localhost", serverEndpoint.getPort(), TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final Queue>> queue = new LinkedList<>(); + for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); + queue.add(streamEndpoint.execute( + new BasicRequestProducer(request, null), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null)); + } + while (!queue.isEmpty()) { + final Future> future = queue.remove(); + final Message result = future.get(LONG_TIMEOUT.getDuration(), LONG_TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result); + final HttpResponse response = result.getHead(); + Assertions.assertNotNull(response); + Assertions.assertEquals(200, response.getCode()); + } + } + + void testHeaderTooLarge(final String method) throws Exception { + final HttpTestServer server = server(); + final HttpTestClient client = client(); + + server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); + final InetSocketAddress serverEndpoint = server.start(); + + final HttpHost target = target(serverEndpoint); + + client.start(); + + final Future connectFuture = client.connect(target, TIMEOUT); + final ClientSessionEndpoint streamEndpoint = connectFuture.get(); + + final int n = 1000; + final StringBuilder buf = new StringBuilder(n); + for (int i = 0; i < n; i++) { + buf.append('a' + i % 10); + } + final String s = buf.toString(); + + final BasicHttpRequest request = BasicRequestBuilder.create(method) + .setHttpHost(target) + .setPath("/hello") + .setHeader("big-f-header", s) + .build(); + + final AsyncEntityProducer entityProducer; + if (Method.POST.isSame(method)) { + entityProducer = AsyncEntityProducers.create(s, ContentType.TEXT_PLAIN); + } else { + entityProducer = null; + } + + final Future> future1 = streamEndpoint.execute( + new BasicRequestProducer(request, entityProducer), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); + final Message result1 = future1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertNotNull(result1); + final HttpResponse response1 = result1.getHead(); + Assertions.assertNotNull(response1); + Assertions.assertEquals(431, response1.getCode()); + MatcherAssert.assertThat(result1.getBody(), + CoreMatchers.allOf( + CoreMatchers.containsString("Maximum"), + CoreMatchers.containsString("exceeded"))); + } + +} diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTests.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTests.java index 2801c0cf38..aaff7e275d 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTests.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTests.java @@ -53,6 +53,16 @@ public CoreTransportTls() { } + @Nested + @DisplayName("Core transport (HTTP/1.1, TLSv1.3)") + class CoreTransportTls13 extends Http1CoreTransportTest { + + public CoreTransportTls13() { + super(URIScheme.HTTPS, "TLSv1.3"); + } + + } + @Nested @DisplayName("Core transport (H2)") class CoreTransportH2 extends H2CoreTransportTest { @@ -73,6 +83,16 @@ public CoreTransportH2Tls() { } + @Nested + @DisplayName("Core transport (H2, TLSv1.3)") + class CoreTransportH2Tls13 extends H2CoreTransportTest { + + public CoreTransportH2Tls13() { + super(URIScheme.HTTPS, "TLSv1.3"); + } + + } + @Nested @DisplayName("Core transport (H2, multiplexing)") class CoreTransportH2Multiplexing extends H2CoreTransportMultiplexingTest { @@ -133,43 +153,4 @@ public AuthenticationImmediateResponse() { } - @Nested - @DisplayName("Core transport (HTTP/1.1, SOCKS)") - class CoreTransportSocksProxy extends Http1SocksProxyCoreTransportTest { - - public CoreTransportSocksProxy() { - super(URIScheme.HTTP); - } - - } - - @Nested - @DisplayName("Core transport (HTTP/1.1, TLS, SOCKS)") - class CoreTransportSocksProxyTls extends Http1SocksProxyCoreTransportTest { - - public CoreTransportSocksProxyTls() { - super(URIScheme.HTTPS); - } - - } - - @Nested - @DisplayName("Core transport (H2, SOCKS)") - class CoreTransportH2SocksProxy extends H2SocksProxyCoreTransportTest { - - public CoreTransportH2SocksProxy() { - super(URIScheme.HTTP); - } - - } - - @Nested - @DisplayName("Core transport (H2, TLS, SOCKS)") - class CoreTransportH2SocksProxyTls extends H2SocksProxyCoreTransportTest { - - public CoreTransportH2SocksProxyTls() { - super(URIScheme.HTTPS); - } - - } } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/JSSEProviderIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/JSSEProviderIntegrationTest.java index e82dd09603..36a3d9f5c0 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/JSSEProviderIntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/JSSEProviderIntegrationTest.java @@ -28,8 +28,6 @@ package org.apache.hc.core5.testing.nio; import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.security.Provider; import java.security.SecureRandom; @@ -40,15 +38,17 @@ import java.util.concurrent.Future; import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.Message; -import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.message.BasicHttpRequest; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http.protocol.DefaultHttpProcessor; import org.apache.hc.core5.http.protocol.RequestValidateHost; +import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.ssl.SSLContextBuilder; import org.apache.hc.core5.util.TimeValue; @@ -195,12 +195,8 @@ public void afterEach(final ExtensionContext context) throws Exception { @Order(3) private final ClientResource clientResource = new ClientResource(); - private URI createRequestURI(final InetSocketAddress serverEndpoint, final String path) { - try { - return new URI("https", null, "localhost", serverEndpoint.getPort(), path, null, null); - } catch (final URISyntaxException e) { - throw new IllegalStateException(); - } + private HttpHost target(final InetSocketAddress serverEndpoint) { + return new HttpHost("https", null, "localhost", serverEndpoint.getPort()); } @Test @@ -208,14 +204,19 @@ void testSimpleGet() throws Exception { server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final Future connectFuture = client.connect( - "localhost", serverEndpoint.getPort(), TIMEOUT); + final Future connectFuture = client.connect(target, TIMEOUT); final ClientSessionEndpoint streamEndpoint = connectFuture.get(); for (int i = 0; i < REQ_NUM; i++) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), + new BasicRequestProducer(request, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); Assertions.assertNotNull(result); @@ -232,14 +233,17 @@ void testSimpleGetConnectionClose() throws Exception { server.register("/hello", () -> new SingleLineResponseHandler("Hi there")); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); - final URI requestURI = createRequestURI(serverEndpoint, "/hello"); for (int i = 0; i < REQ_NUM; i++) { final Future connectFuture = client.connect( "localhost", serverEndpoint.getPort(), TIMEOUT); try (final ClientSessionEndpoint streamEndpoint = connectFuture.get()) { final Future> future = streamEndpoint.execute( - AsyncRequestBuilder.get(requestURI) + AsyncRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") .addHeader(HttpHeaders.CONNECTION, "close") .build(), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); @@ -260,14 +264,20 @@ void testSimpleGetIdentityTransfer() throws Exception { server.configure(new DefaultHttpProcessor(new RequestValidateHost())); final InetSocketAddress serverEndpoint = server.start(); + final HttpHost target = target(serverEndpoint); + client.start(); for (int i = 0; i < REQ_NUM; i++) { final Future connectFuture = client.connect( "localhost", serverEndpoint.getPort(), TIMEOUT); try (final ClientSessionEndpoint streamEndpoint = connectFuture.get()) { + final BasicHttpRequest request = BasicRequestBuilder.get() + .setHttpHost(target) + .setPath("/hello") + .build(); final Future> future = streamEndpoint.execute( - new BasicRequestProducer(Method.GET, createRequestURI(serverEndpoint, "/hello")), + new BasicRequestProducer(request, null), new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null); final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); Assertions.assertNotNull(result); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/SingleLineResponseHandler.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/SingleLineResponseHandler.java index dd4d6ee870..5506aae60b 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/SingleLineResponseHandler.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/SingleLineResponseHandler.java @@ -51,7 +51,7 @@ public AsyncRequestConsumer> prepare( final HttpRequest request, final EntityDetails entityDetails, final HttpContext context) throws HttpException { - return new BasicRequestConsumer<>(entityDetails != null? new StringAsyncEntityConsumer() : null); + return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); } @Override diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java index e0acd9685d..138baf6c6b 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSIntegrationTest.java @@ -40,6 +40,7 @@ import javax.net.ssl.SSLSession; import org.apache.hc.core5.concurrent.BasicFuture; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.function.Supplier; @@ -146,6 +147,7 @@ HttpAsyncServer createServer(final TlsStrategy tlsStrategy) { .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) .setExceptionCallback(LoggingExceptionCallback.INSTANCE) .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE) .setRequestRouter(RequestRouter.>builder() .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", () -> new EchoHandler(2048)) .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) @@ -164,6 +166,7 @@ HttpAsyncRequester createClient(final TlsStrategy tlsStrategy) { .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE) .setExceptionCallback(LoggingExceptionCallback.INSTANCE) .setIOSessionListener(LoggingIOSessionListener.INSTANCE) + .setIOReactorMetricsListener(LoggingReactorMetricsListener.INSTANCE) .create(); } @@ -183,16 +186,9 @@ Future executeTlsHandshake() throws Exception { @Override public void completed(final AsyncClientEndpoint clientEndpoint) { try { - ((TlsUpgradeCapable) clientEndpoint). tlsUpgrade( + ((TlsUpgradeCapable) clientEndpoint).tlsUpgrade( target, - new FutureContribution(tlsFuture) { - - @Override - public void completed(final ProtocolIOSession protocolIOSession) { - tlsFuture.completed(protocolIOSession.getTlsDetails()); - } - - }); + new CompletingFutureContribution<>(tlsFuture, ProtocolIOSession::getTlsDetails)); } catch (final Exception ex) { tlsFuture.failed(ex); } @@ -374,7 +370,7 @@ void testHostNameVerification() throws Exception { final ListenerEndpoint listener = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); final InetSocketAddress address = (InetSocketAddress) listener.getAddress(); - final HttpHost target1 = new HttpHost(URIScheme.HTTPS.id, InetAddress.getLocalHost(), "localhost", address.getPort()); + final HttpHost target1 = new HttpHost(URIScheme.HTTPS.id, InetAddress.getLoopbackAddress(), "localhost", address.getPort()); final Future> resultFuture1 = client.execute( target1, new BasicRequestProducer(Method.POST, target1, "/stuff", @@ -384,7 +380,7 @@ void testHostNameVerification() throws Exception { Assertions.assertNotNull(message1); Assertions.assertEquals(200, message1.getHead().getCode()); - final HttpHost target2 = new HttpHost(URIScheme.HTTPS.id, InetAddress.getLocalHost(), "some-other-host", address.getPort()); + final HttpHost target2 = new HttpHost(URIScheme.HTTPS.id, InetAddress.getLoopbackAddress(), "some-other-host", address.getPort()); final Future> resultFuture2 = client.execute( target2, new BasicRequestProducer(Method.POST, target2, "/stuff", diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSUpgradeTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSUpgradeTest.java index 1c554e33c5..025f04b1fd 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSUpgradeTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/TLSUpgradeTest.java @@ -33,7 +33,7 @@ import java.util.concurrent.Future; import org.apache.hc.core5.concurrent.BasicFuture; -import org.apache.hc.core5.concurrent.FutureContribution; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpHost; @@ -52,10 +52,13 @@ import org.apache.hc.core5.http.nio.ssl.TlsUpgradeCapable; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.HttpAsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.HttpAsyncServerResource; import org.apache.hc.core5.util.Timeout; @@ -74,7 +77,9 @@ class TLSUpgradeTest { private final HttpAsyncRequesterResource clientResource; public TLSUpgradeTest() { - this.serverResource = new HttpAsyncServerResource(bootstrap -> bootstrap + this.serverResource = new HttpAsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(TIMEOUT) @@ -84,7 +89,9 @@ public TLSUpgradeTest() { .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) ); - this.clientResource = new HttpAsyncRequesterResource(bootstrap -> bootstrap + this.clientResource = new HttpAsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(TIMEOUT) .build()) @@ -120,14 +127,7 @@ void testTLSUpgrade() throws Exception { // Upgrade to TLS final BasicFuture tlsFuture = new BasicFuture<>(null); - ((TlsUpgradeCapable) clientEndpoint).tlsUpgrade(target, new FutureContribution(tlsFuture) { - - @Override - public void completed(final ProtocolIOSession protocolIOSession) { - tlsFuture.completed(protocolIOSession.getTlsDetails()); - } - - }); + ((TlsUpgradeCapable) clientEndpoint).tlsUpgrade(target, new CompletingFutureContribution<>(tlsFuture, ProtocolIOSession::getTlsDetails)); final TlsDetails tlsDetails = tlsFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); Assertions.assertNotNull(tlsDetails); diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/reactive/ReactiveClientTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/reactive/ReactiveClientTest.java index e4bf3a9485..c89b316e70 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/reactive/ReactiveClientTest.java +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/reactive/ReactiveClientTest.java @@ -59,11 +59,14 @@ import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.ssl.H2ClientTlsStrategy; +import org.apache.hc.core5.http2.ssl.H2ServerTlsStrategy; import org.apache.hc.core5.reactive.ReactiveEntityProducer; import org.apache.hc.core5.reactive.ReactiveResponseConsumer; import org.apache.hc.core5.reactive.ReactiveServerExchangeHandler; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.testing.SSLTestContexts; import org.apache.hc.core5.testing.extension.nio.H2AsyncRequesterResource; import org.apache.hc.core5.testing.extension.nio.H2AsyncServerResource; import org.apache.hc.core5.testing.reactive.Reactive3TestUtils.StreamDescription; @@ -71,6 +74,7 @@ import org.apache.hc.core5.util.Timeout; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.reactivestreams.Publisher; @@ -90,8 +94,10 @@ abstract class ReactiveClientTest { public ReactiveClientTest(final HttpVersionPolicy httpVersionPolicy) { this.versionPolicy = httpVersionPolicy; - this.serverResource = new H2AsyncServerResource(bootstrap -> bootstrap + this.serverResource = new H2AsyncServerResource(); + this.serverResource.configure(bootstrap -> bootstrap .setVersionPolicy(versionPolicy) + .setTlsStrategy(new H2ServerTlsStrategy(SSLTestContexts.createServerSSLContext())) .setIOReactorConfig( IOReactorConfig.custom() .setSoTimeout(SOCKET_TIMEOUT) @@ -101,8 +107,10 @@ public ReactiveClientTest(final HttpVersionPolicy httpVersionPolicy) { .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) .build()) ); - this.clientResource = new H2AsyncRequesterResource(bootstrap -> bootstrap + this.clientResource = new H2AsyncRequesterResource(); + this.clientResource.configure(bootstrap -> bootstrap .setVersionPolicy(versionPolicy) + .setTlsStrategy(new H2ClientTlsStrategy(SSLTestContexts.createClientSSLContext())) .setIOReactorConfig(IOReactorConfig.custom() .setSoTimeout(SOCKET_TIMEOUT) .build()) @@ -207,7 +215,7 @@ void testRequestError() throws Exception { Assertions.assertSame(exceptionThrown, exception.getCause().getCause()); } - @Test + @Test @Disabled void testRequestTimeout() throws Exception { final InetSocketAddress address = startServer(); final HttpAsyncRequester requester = clientResource.start(); @@ -233,7 +241,7 @@ void testRequestTimeout() throws Exception { } } - @Test + @Test @Disabled void testResponseCancellation() throws Exception { final InetSocketAddress address = startServer(); final HttpAsyncRequester requester = clientResource.start(); diff --git a/httpcore5-testing/src/test/resources/docker/BUILDING.txt b/httpcore5-testing/src/test/resources/docker/BUILDING.txt new file mode 100644 index 0000000000..0e5664f7fe --- /dev/null +++ b/httpcore5-testing/src/test/resources/docker/BUILDING.txt @@ -0,0 +1,66 @@ += SSL key / cert material + +Execute in the project root + +# Issue a certificate request +--- +openssl req -config test-CA/openssl.cnf -new -nodes -sha256 -days 36500 \ + -subj '/O=Apache Software Foundation/OU=HttpComponents Project/CN=localhost/emailAddress=dev@hc.apache.org/' \ + -addext 'subjectAltName = DNS:localhost' \ + -keyout httpcore5-testing/src/test/resources/docker/server-key.pem \ + -out httpcore5-testing/src/test/resources/docker/server-certreq.pem +--- +# Verify the request +--- +openssl req -in httpcore5-testing/src/test/resources/docker/server-certreq.pem -text -noout +--- +# Sign new certificate with the test CA key +--- +openssl ca -config test-CA/openssl.cnf -days 36500 \ + -out httpcore5-testing/src/test/resources/docker/server-cert.pem \ + -in httpcore5-testing/src/test/resources/docker/server-certreq.pem \ + && rm httpcore5-testing/src/test/resources/docker/server-certreq.pem +--- + +# Export the certificate and the key into P12 store + +--- +openssl pkcs12 -export -out httpcore5-testing/src/test/resources/docker/server.p12 \ + -CAfile test-CA/ca-cert.pem \ + -in httpcore5-testing/src/test/resources/docker/server-cert.pem \ + -inkey httpcore5-testing/src/test/resources/docker/server-key.pem \ + -passin pass:nopassword -passout pass:nopassword +--- + +# Verify the P12 store + +--- +openssl pkcs12 -info -in httpcore5-testing/src/test/resources/docker/server.p12 \ + -passin pass:nopassword -passout pass:nopassword +--- + +--- +keytool -list -keystore httpcore5-testing/src/test/resources/docker/server.p12 -storepass nopassword +--- + +# Create JKS store with the Test CA cert +--- +keytool -import -trustcacerts -alias test-ca -file test-CA/ca-cert.pem -keystore httpcore5-testing/src/test/resources/test-ca.jks -storepass nopassword +--- + += Running a local version of HttpBin + +# Create container +--- +docker container create -it --name test-httpbin -p 8080:80 kennethreitz/httpbin:latest +docker start test-httpbin +docker logs test-httpbin +--- +# Trouble-shoot container +--- +docker exec -it test-httpbin bash +--- +# Delete container +--- +docker rm --force test-httpbin +--- \ No newline at end of file diff --git a/httpcore5-testing/docker/apache-httpd/httpd-vhosts.conf b/httpcore5-testing/src/test/resources/docker/httpd/httpd-default.conf similarity index 66% rename from httpcore5-testing/docker/apache-httpd/httpd-vhosts.conf rename to httpcore5-testing/src/test/resources/docker/httpd/httpd-default.conf index 160c1cc79b..8465bdf60f 100644 --- a/httpcore5-testing/docker/apache-httpd/httpd-vhosts.conf +++ b/httpcore5-testing/src/test/resources/docker/httpd/httpd-default.conf @@ -13,15 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -ServerName localhost:80 - -Protocols h2c http/1.1 - - - LogLevel http2:info - H2Push on - - AllowOverride none Require all denied @@ -34,12 +25,7 @@ DocumentRoot "/var/httpd/www" Require all granted - - - Header add Link ";rel=preload" - Header add Link ";rel=preload" - Header add Link ";rel=preload" - Header add Link ";rel=preload" - Header add Link ";rel=preload" - - + + ServerName localhost:80 + Protocols http/1.1 + diff --git a/httpcore5-testing/docker/nginx/Dockerfile b/httpcore5-testing/src/test/resources/docker/httpd/httpd-h2c.conf similarity index 65% rename from httpcore5-testing/docker/nginx/Dockerfile rename to httpcore5-testing/src/test/resources/docker/httpd/httpd-h2c.conf index 829587d20d..9091a37dd7 100644 --- a/httpcore5-testing/docker/nginx/Dockerfile +++ b/httpcore5-testing/src/test/resources/docker/httpd/httpd-h2c.conf @@ -13,18 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM nginx:1.22 -MAINTAINER dev@hc.apache.org +Listen 81 -ENV var_dir /var/nginx -ENV www_dir ${var_dir}/www + + ServerName localhost:81 + Protocols h2c -RUN apt-get update -RUN apt-get install -y subversion + + LogLevel http2:info + H2Push on + H2EarlyHints on -RUN mkdir -p ${var_dir} -RUN svn co --depth immediates http://svn.apache.org/repos/asf/httpcomponents/site ${www_dir} -RUN svn up --set-depth infinity ${www_dir}/images -RUN svn up --set-depth infinity ${www_dir}/css + + H2PushResource /aaa + H2PushResource /bbb + H2PushResource /ccc + + -COPY default.conf /etc/nginx/conf.d/default.conf + diff --git a/httpcore5-testing/src/test/resources/docker/httpd/httpd-ssl.conf b/httpcore5-testing/src/test/resources/docker/httpd/httpd-ssl.conf new file mode 100644 index 0000000000..d9722d24cf --- /dev/null +++ b/httpcore5-testing/src/test/resources/docker/httpd/httpd-ssl.conf @@ -0,0 +1,53 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +Listen 443 + +SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES +SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES + +SSLHonorCipherOrder on + +SSLProtocol all -SSLv3 +SSLProxyProtocol all -SSLv3 + +SSLPassPhraseDialog builtin + +SSLSessionCacheTimeout 300 + + + ServerName localhost:443 + Protocols h2 http/1.1 + + SSLEngine on + SSLCertificateFile "/usr/local/apache2/conf/server-cert.pem" + SSLCertificateKeyFile "/usr/local/apache2/conf/server-key.pem" + CustomLog /proc/self/fd/1 \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + LogLevel http2:info + H2Push on + H2EarlyHints on + + + H2PushResource /aaa + H2PushResource /bbb + H2PushResource /ccc + + + + diff --git a/httpcore5-testing/docker/httpbin/Dockerfile b/httpcore5-testing/src/test/resources/docker/nginx/default.conf similarity index 76% rename from httpcore5-testing/docker/httpbin/Dockerfile rename to httpcore5-testing/src/test/resources/docker/nginx/default.conf index e4120c33cb..99b2ba4b7c 100644 --- a/httpcore5-testing/docker/httpbin/Dockerfile +++ b/httpcore5-testing/src/test/resources/docker/nginx/default.conf @@ -13,5 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM kennethreitz/httpbin:latest -MAINTAINER dev@hc.apache.org +server { + listen 80; + server_name localhost; + + location / { + root /var/nginx/www; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} + diff --git a/httpcore5-testing/docker/nginx/default.conf b/httpcore5-testing/src/test/resources/docker/nginx/h2c.conf similarity index 74% rename from httpcore5-testing/docker/nginx/default.conf rename to httpcore5-testing/src/test/resources/docker/nginx/h2c.conf index b6160d6842..f994f2f473 100644 --- a/httpcore5-testing/docker/nginx/default.conf +++ b/httpcore5-testing/src/test/resources/docker/nginx/h2c.conf @@ -14,19 +14,16 @@ # limitations under the License. server { - listen 80 http2; + listen 81 http2; server_name localhost; location / { root /var/nginx/www; - index index.html; - location = /index.html { - http2_push "/css/site.css"; - http2_push "/css/maven-theme.css"; - http2_push "/css/maven-base.css"; - http2_push "/css/hc-maven.css"; - http2_push "/images/logos/httpcomponents.png"; + location = /pushy { + http2_push "/aaa"; + http2_push "/bbb"; + http2_push "/ccc"; } } diff --git a/httpcore5-testing/docker-compose.yml b/httpcore5-testing/src/test/resources/docker/nginx/ssl.conf similarity index 53% rename from httpcore5-testing/docker-compose.yml rename to httpcore5-testing/src/test/resources/docker/nginx/ssl.conf index 2162cb1012..e0d83e10aa 100644 --- a/httpcore5-testing/docker-compose.yml +++ b/httpcore5-testing/src/test/resources/docker/nginx/ssl.conf @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -13,21 +13,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -version: '3.5' +server { + listen 443 ssl http2; + server_name localhost; + ssl_certificate /etc/nginx/server-cert.pem; + ssl_certificate_key /etc/nginx/server-key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + root /var/nginx/www; + + location = /pushy { + http2_push "/aaa"; + http2_push "/bbb"; + http2_push "/ccc"; + } + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} -services: - test-httpd: - container_name: "my-hc-tests-httpd" - image: "hc-tests-httpd:latest" - ports: - - "8080:80" - test-nginx: - container_name: "my-hc-tests-nginx" - image: "hc-tests-nginx:latest" - ports: - - "8081:80" - test-httpbin: - container_name: "my-hc-tests-httpbin" - image: "hc-tests-httpbin:latest" - ports: - - "8082:80" diff --git a/httpcore5-testing/src/test/resources/docker/server-cert.pem b/httpcore5-testing/src/test/resources/docker/server-cert.pem new file mode 100644 index 0000000000..03bed220da --- /dev/null +++ b/httpcore5-testing/src/test/resources/docker/server-cert.pem @@ -0,0 +1,85 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: sha256WithRSAEncryption + Issuer: O=Apache Software Foundation, OU=HttpComponents Project, CN=Test CA/emailAddress=dev@hc.apache.org + Validity + Not Before: Oct 20 19:34:55 2024 GMT + Not After : Sep 26 19:34:55 2124 GMT + Subject: O=Apache Software Foundation, OU=HttpComponents Project, CN=localhost/emailAddress=dev@hc.apache.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:c9:0d:66:dc:9c:9c:57:43:83:ce:0a:09:d6:58: + b3:05:3e:ed:c8:c4:b9:3e:79:66:22:dc:29:9b:a6: + 8e:cc:22:b6:3d:96:d9:11:78:1c:cd:73:37:7a:5b: + a6:91:32:4d:c4:11:ee:d0:97:5d:4e:89:0f:c7:cb: + 79:40:e4:1e:d2:d7:f8:4f:0f:37:7e:04:71:a9:74: + 40:e6:17:1c:94:65:f9:13:0d:52:bc:74:fe:6d:e6: + ad:f0:07:60:81:e0:7e:b9:8c:52:8b:ca:ec:2f:04: + 9f:5e:e5:2a:26:70:6d:e9:68:f1:9e:8c:d0:06:2f: + 50:ee:98:03:d9:ba:82:5b:65:01:80:60:13:09:da: + 64:be:ae:76:c4:ee:5c:05:fd:5d:35:fd:7e:91:29: + 22:5d:f2:18:2f:49:f9:f9:c0:71:a0:85:26:60:09: + d6:56:42:29:33:95:5b:e2:d8:02:af:89:02:f6:01: + a7:cb:bc:90:bb:20:1e:6d:ee:9f:c0:ae:24:df:be: + 30:5c:89:5d:c1:a4:21:a1:06:89:75:0a:16:f3:34: + ef:fc:a3:a6:7d:9c:6a:97:28:59:1e:16:98:79:a1: + 7e:b3:0a:63:7e:2d:4d:6c:eb:40:c6:c9:ea:63:01: + 02:78:4a:4a:eb:98:8c:61:d7:d6:df:9a:cf:a4:8c: + 32:21 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 76:DC:5B:ED:CF:E7:5A:EC:05:2C:35:5B:42:25:EA:45:E8:18:AB:CE + X509v3 Authority Key Identifier: + 03:E4:E7:DA:0F:64:DB:13:1E:BD:85:AB:76:BC:29:CA:2F:A7:C7:4B + X509v3 Subject Alternative Name: + DNS:localhost + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 09:f8:61:1a:59:c2:a4:28:f2:f0:b1:0a:41:59:14:f5:3a:32: + f0:19:bc:d8:c0:21:94:d0:6e:e7:c3:7b:df:0f:52:b1:d6:b8: + 78:5d:c5:d7:f4:f1:3d:85:ea:95:80:5c:eb:55:e0:8b:d3:02: + ae:e5:50:85:0d:7e:98:fa:0d:1d:48:ff:74:ba:3b:c8:97:f9: + c6:bb:ce:f1:1d:9e:80:f2:45:6c:14:e2:85:b0:5b:ea:9b:20: + cf:d0:25:20:5d:41:51:c2:cc:a8:ff:b1:23:26:cd:c5:52:64: + 5b:a1:2d:a6:27:e9:28:3f:09:d0:d9:b1:49:1c:1d:ad:a3:06: + 3d:09:f0:dd:e6:fc:7c:83:b5:a5:e9:d7:99:9a:17:8e:e7:d4: + 18:60:cf:14:56:6d:01:6b:9a:cf:9d:cd:1b:50:78:06:d9:4e: + f2:d8:38:16:6a:5f:8b:07:44:ae:e9:92:5f:54:7c:9e:05:18: + 5d:e1:e5:dd:83:88:8e:6c:02:32:a9:56:0d:55:06:2e:dc:27: + 96:56:ed:65:59:c8:17:ac:18:f5:dd:65:b0:43:c3:d0:d1:6d: + ca:2c:06:5e:9a:1d:33:33:57:da:aa:17:4e:a7:b2:63:93:45: + ef:e6:b4:ca:93:d5:e8:e2:31:64:c8:26:7b:df:90:d8:b0:36: + 48:3b:df:b1 +-----BEGIN CERTIFICATE----- +MIIEBjCCAu6gAwIBAgIBADANBgkqhkiG9w0BAQsFADB6MSMwIQYDVQQKDBpBcGFj +aGUgU29mdHdhcmUgRm91bmRhdGlvbjEfMB0GA1UECwwWSHR0cENvbXBvbmVudHMg +UHJvamVjdDEQMA4GA1UEAwwHVGVzdCBDQTEgMB4GCSqGSIb3DQEJARYRZGV2QGhj +LmFwYWNoZS5vcmcwIBcNMjQxMDIwMTkzNDU1WhgPMjEyNDA5MjYxOTM0NTVaMHwx +IzAhBgNVBAoMGkFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMR8wHQYDVQQLDBZI +dHRwQ29tcG9uZW50cyBQcm9qZWN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxIDAeBgkq +hkiG9w0BCQEWEWRldkBoYy5hcGFjaGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAyQ1m3JycV0ODzgoJ1lizBT7tyMS5PnlmItwpm6aOzCK2PZbZ +EXgczXM3elumkTJNxBHu0JddTokPx8t5QOQe0tf4Tw83fgRxqXRA5hcclGX5Ew1S +vHT+beat8AdggeB+uYxSi8rsLwSfXuUqJnBt6WjxnozQBi9Q7pgD2bqCW2UBgGAT +Cdpkvq52xO5cBf1dNf1+kSkiXfIYL0n5+cBxoIUmYAnWVkIpM5Vb4tgCr4kC9gGn +y7yQuyAebe6fwK4k374wXIldwaQhoQaJdQoW8zTv/KOmfZxqlyhZHhaYeaF+swpj +fi1NbOtAxsnqYwECeEpK65iMYdfW35rPpIwyIQIDAQABo4GSMIGPMAkGA1UdEwQC +MAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl +MB0GA1UdDgQWBBR23Fvtz+da7AUsNVtCJepF6BirzjAfBgNVHSMEGDAWgBQD5Ofa +D2TbEx69hat2vCnKL6fHSzAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcN +AQELBQADggEBAAn4YRpZwqQo8vCxCkFZFPU6MvAZvNjAIZTQbufDe98PUrHWuHhd +xdf08T2F6pWAXOtV4IvTAq7lUIUNfpj6DR1I/3S6O8iX+ca7zvEdnoDyRWwU4oWw +W+qbIM/QJSBdQVHCzKj/sSMmzcVSZFuhLaYn6Sg/CdDZsUkcHa2jBj0J8N3m/HyD +taXp15maF47n1BhgzxRWbQFrms+dzRtQeAbZTvLYOBZqX4sHRK7pkl9UfJ4FGF3h +5d2DiI5sAjKpVg1VBi7cJ5ZW7WVZyBesGPXdZbBDw9DRbcosBl6aHTMzV9qqF06n +smOTRe/mtMqT1ejiMWTIJnvfkNiwNkg737E= +-----END CERTIFICATE----- diff --git a/httpcore5-testing/src/test/resources/docker/server-key.pem b/httpcore5-testing/src/test/resources/docker/server-key.pem new file mode 100644 index 0000000000..0d70351424 --- /dev/null +++ b/httpcore5-testing/src/test/resources/docker/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJDWbcnJxXQ4PO +CgnWWLMFPu3IxLk+eWYi3Cmbpo7MIrY9ltkReBzNczd6W6aRMk3EEe7Ql11OiQ/H +y3lA5B7S1/hPDzd+BHGpdEDmFxyUZfkTDVK8dP5t5q3wB2CB4H65jFKLyuwvBJ9e +5SomcG3paPGejNAGL1DumAPZuoJbZQGAYBMJ2mS+rnbE7lwF/V01/X6RKSJd8hgv +Sfn5wHGghSZgCdZWQikzlVvi2AKviQL2AafLvJC7IB5t7p/AriTfvjBciV3BpCGh +Bol1ChbzNO/8o6Z9nGqXKFkeFph5oX6zCmN+LU1s60DGyepjAQJ4SkrrmIxh19bf +ms+kjDIhAgMBAAECggEAMWAwNdfkcW3sTcqbVd/cv1oQDbF6iQRJeCA3u7kw8qNH +vGJylt63vCEiq9McWiZPgOqiiunRiXsRvBCVi8E6bK+LlOb4yuMOgv5A/gEJDMzq +Wap4+j2FSrpPV1aIdf0LQkha8Wf5jyaEeUqwilYsOBmE1VphsFMofiApOeybHRSG +1PWcPXUYv/ylLHdEKJOy0eKgIkCSHfIyPzGVvVpfKCQYu9qQSXpx9ivNNvIrjgPZ +i5lP3yYm7SKKGTb2J6eYsSVfvH8Q1bob8EV0ZJKfbqioZmwIQ0F+UCjR4j5HbYUR +ygz2FQn9nOAqJaHbeKIj/qRi1V768TXMbW3H7cW5/QKBgQDkubAgEVhLwWJk8bTm +sfrCpLkRaL0Dzen021sX9R5g3ajofL7INtDiCeP/5HSo8ffQdY9Q4CJjS12ekdjA +F5sDT8px3phPPk2UEc1MtqeaXnL9coo243Mu7aENb1OsiDQKJ6TCXKE7QqR6sj9I +B9rI36aKIweG6nVPVlzn0LREdwKBgQDhBvBQ1BAtvq/DXHdurqBdXpXIdS/xWc4E +GVsKsInhg3Tr1PUhjwpIUG02uyIdJzy+6K2UjAqQoo4B5D55hWL4tHx0S2mt38g4 +LCAs6s8Gr0E9Gnuwij5Up8bag3rTtMmjk0pFDcJtAQFMQ0WiXTkkgklKYuavCFIK +h9rO6R1cJwKBgQC2lXR/ZNkzQCCnrDtYnWMr1grWVuHsE4hbqm/BZC7n7IpVbJ9v +fDKq/nI/Z8Ooyd+lTPMnAITy9sq5Nnvse+uGbT+SPrsfJwEO3lcgkf6hQBxTLggf +YNol8BPMgb4t0FyabqMbdI5QnBZoy7mwanTAPajYRLZRgQA8YRixBO2iaQKBgFGz +xj9ir7kcOg7RnN+H8dvUwsd0nQKhW6arWh3oeTdzFlmmCZa0q8QTx4OOsFUrcOfT +7Wo46oEXND5Fk9Vlc8jfJyzGUMl7reOPSeNlIePIcARygCRaHUV5YT7nbAo/4tJM +YTPvSf9v1PvOlRLdjCjQUTH79MvqVndSWkSz6SATAoGBAM0RSRuSOI+/AhPUKBc8 +kknh5GF7fTAtAABwj5ruvqaNzTGwtJWyge9W+ZxuiCC+oFCMmslzOzvXVA/Uwmk0 +BnPR5eZZm8/LwGHr0Npk8D9VrCE5KmyhpBFczXxVNXZ/KBduZu//UFxPhWim/bzl +3pArPBZ0o9eeHjxbch5ct2uF +-----END PRIVATE KEY----- diff --git a/httpcore5-testing/src/test/resources/docker/server.p12 b/httpcore5-testing/src/test/resources/docker/server.p12 new file mode 100644 index 0000000000..a0410eaee2 Binary files /dev/null and b/httpcore5-testing/src/test/resources/docker/server.p12 differ diff --git a/httpcore5-testing/src/test/resources/test-ca.jks b/httpcore5-testing/src/test/resources/test-ca.jks new file mode 100644 index 0000000000..fb70ffb156 Binary files /dev/null and b/httpcore5-testing/src/test/resources/test-ca.jks differ diff --git a/httpcore5/pom.xml b/httpcore5/pom.xml index 3f176fcf0e..3cf0cde5cc 100644 --- a/httpcore5/pom.xml +++ b/httpcore5/pom.xml @@ -28,7 +28,7 @@ org.apache.httpcomponents.core5 httpcore5-parent - 5.3.1-SNAPSHOT + 5.4-alpha1-SNAPSHOT httpcore5 Apache HttpComponents Core HTTP/1.1 diff --git a/httpcore5/src/main/java/org/apache/hc/core5/annotation/Experimental.java b/httpcore5/src/main/java/org/apache/hc/core5/annotation/Experimental.java index 6669be7938..9781ca8403 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/annotation/Experimental.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/annotation/Experimental.java @@ -36,7 +36,7 @@ * The field or method to which this annotation is applied is marked as experimental. */ @Documented -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.CLASS) public @interface Experimental { } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/concurrent/CompletingFutureContribution.java b/httpcore5/src/main/java/org/apache/hc/core5/concurrent/CompletingFutureContribution.java new file mode 100644 index 0000000000..79ff607d53 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/concurrent/CompletingFutureContribution.java @@ -0,0 +1,70 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.concurrent; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Completes a result on a {@link BasicFuture}. + * + * @param the future result type of an asynchronous operation for this type. + * @param the future result type of an asynchronous operation for the {@link FutureCallback}. + * @since 5.4 + */ +public class CompletingFutureContribution extends FutureContribution { + + private final Function resultProvider; + + /** + * Constructs a new instance to callback the given {@link BasicFuture}. + * + * @param future The callback. + */ + @SuppressWarnings("unchecked") + public CompletingFutureContribution(final BasicFuture future) { + this(future, (Function) Function.identity()); + } + + /** + * Constructs a new instance to callback the given {@link BasicFuture}. + * + * @param future The callback. + * @param resultProvider Provides the result to complete the future. + */ + public CompletingFutureContribution(final BasicFuture future, final Function resultProvider) { + super(future); + this.resultProvider = Objects.requireNonNull(resultProvider, "resultProvider"); + } + + @Override + public void completed(final T result) { + getFuture().completed(resultProvider.apply(result)); + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/concurrent/DefaultThreadFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/concurrent/DefaultThreadFactory.java index 4ad6a6e01d..93e2e1eaa3 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/concurrent/DefaultThreadFactory.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/concurrent/DefaultThreadFactory.java @@ -58,7 +58,7 @@ public DefaultThreadFactory(final String namePrefix) { @Override public Thread newThread(final Runnable target) { - final Thread thread = new Thread(this.group, target, this.namePrefix + "-" + this.count.incrementAndGet()); + final Thread thread = new Thread(this.group, target, this.namePrefix + "-" + this.count.incrementAndGet()); thread.setDaemon(daemon); return thread; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/concurrent/FutureContribution.java b/httpcore5/src/main/java/org/apache/hc/core5/concurrent/FutureContribution.java index 88cd68b5b2..de1f68eb9b 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/concurrent/FutureContribution.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/concurrent/FutureContribution.java @@ -26,11 +26,13 @@ */ package org.apache.hc.core5.concurrent; +import java.util.Objects; + /** * Convenience base class for {@link FutureCallback}s that contribute a result * of the operation to another {@link BasicFuture}. * - * @param the future result type of an asynchronous operation. + * @param the future result type of an asynchronous operation for this type. * @since 5.1 */ public abstract class FutureContribution implements FutureCallback { @@ -40,24 +42,25 @@ public abstract class FutureContribution implements FutureCallback { /** * Constructs a new instance to callback the given {@link BasicFuture}. * - * @param future The callback. + * @param future The callback, non-null. */ public FutureContribution(final BasicFuture future) { - this.future = future; + this.future = Objects.requireNonNull(future); } @Override public final void failed(final Exception ex) { - if (future != null) { - future.failed(ex); - } + future.failed(ex); } @Override public final void cancelled() { - if (future != null) { - future.cancel(); - } + future.cancel(); + } + + @SuppressWarnings("unchecked") + BasicFuture getFuture() { + return (BasicFuture) future; } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/ContentType.java b/httpcore5/src/main/java/org/apache/hc/core5/http/ContentType.java index b8e049961a..31566b5191 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/ContentType.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/ContentType.java @@ -75,6 +75,17 @@ public final class ContentType implements Serializable { public static final ContentType APPLICATION_JSON = create( "application/json", StandardCharsets.UTF_8); + /** + * Public constant media type for ZIP archives {@code application/x-zip-compressed}. + *

+ * Note that Windows uploads {@code .zip} files with this non-standard MIME type. + *

+ * + * @see ZIP archive application/x-zip-compressed + * @since 5.4 + */ + public static final ContentType APPLICATION_ZIP_COMPRESSED = create("application/x-zip-compressed"); + /** * Public constant media type for {@code application/x-ndjson}. * @since 5.1 @@ -412,10 +423,8 @@ public static ContentType parse(final CharSequence s) throws UnsupportedCharsetE * * @param s text * @return content type {@code Content-Type} value or null. - * @throws UnsupportedCharsetException Thrown when the named charset is not available in - * this instance of the Java virtual machine */ - public static ContentType parseLenient(final CharSequence s) throws UnsupportedCharsetException { + public static ContentType parseLenient(final CharSequence s) { return parse(s, false); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpResponseInterceptor.java b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpResponseInterceptor.java index 421011bead..56e0ae61be 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpResponseInterceptor.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpResponseInterceptor.java @@ -38,9 +38,9 @@ * the HTTP protocol. Usually protocol interceptors are expected to act upon * one specific header or a group of related headers of the incoming message * or populate the outgoing message with one specific header or a group of - * related headers. Protocol + * related headers. *

- * Interceptors can also manipulate content entities enclosed with messages. + * Protocol interceptors can also manipulate content entities enclosed with messages. * Usually this is accomplished by using the 'Decorator' pattern where a wrapper * entity class is used to decorate the original entity. *

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpVersion.java b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpVersion.java index a8282f5a5c..f16723bce9 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/HttpVersion.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/HttpVersion.java @@ -59,10 +59,10 @@ public final class HttpVersion extends ProtocolVersion { /** HTTP protocol version 2.0 */ public static final HttpVersion HTTP_2_0 = new HttpVersion(2, 0); - public static final HttpVersion HTTP_2 = HTTP_2_0; + public static final HttpVersion HTTP_2 = HTTP_2_0; /** HTTP/1.1 is default */ - public static final HttpVersion DEFAULT = HTTP_1_1; + public static final HttpVersion DEFAULT = HTTP_1_1; /** * All HTTP versions known to HttpCore. diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java b/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java index 731e72d723..787fb405fc 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/Method.java @@ -29,6 +29,7 @@ import java.util.Locale; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.util.Args; /** @@ -91,7 +92,21 @@ public enum Method { /** * The HTTP {@code PATCH} method is unsafe and non-idempotent. */ - PATCH(false, false); + PATCH(false, false), + + /** + * The HTTP {@code QUERY} method is safe and idempotent. + *

+ * {@code QUERY} is a method defined to represent a safe, idempotent request that carries a body. + * This allows clients to send a body while enjoying the {@code GET}-like properties (such as easy caching, + * safe CORS handling [if supported] and bookmarking), as well as the {@code POST}-like properties (such as + * being able to send a query in a richer format, and not being limited by URI length and escaping restrictions). + * + * @since 5.4 + */ + @Experimental + //("QUERY method is still in DRAFT status") + QUERY(true, true); private final boolean safe; private final boolean idempotent; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/ProtocolVersion.java b/httpcore5/src/main/java/org/apache/hc/core5/http/ProtocolVersion.java index b6247e9535..a1469a299e 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/ProtocolVersion.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/ProtocolVersion.java @@ -71,7 +71,7 @@ public class ProtocolVersion implements Serializable { */ public ProtocolVersion(final String protocol, final int major, final int minor) { this.protocol = Args.notNull(protocol, "Protocol name"); - this.major = Args.notNegative(major, "Protocol minor version"); + this.major = Args.notNegative(major, "Protocol major version"); this.minor = Args.notNegative(minor, "Protocol minor version"); } @@ -148,9 +148,9 @@ public final boolean equals(final Object obj) { } final ProtocolVersion that = (ProtocolVersion) obj; - return (this.protocol.equals(that.protocol) && - (this.major == that.major) && - (this.minor == that.minor)); + return this.protocol.equals(that.protocol) && + this.major == that.major && + this.minor == that.minor; } /** @@ -180,7 +180,7 @@ public String format() { * can be called with the argument, {@code false} otherwise */ public boolean isComparable(final ProtocolVersion that) { - return (that != null) && this.protocol.equals(that.protocol); + return that != null && this.protocol.equals(that.protocol); } @@ -223,7 +223,7 @@ public int compareToVersion(final ProtocolVersion that) { * {@code false} otherwise */ public final boolean greaterEquals(final ProtocolVersion version) { - return isComparable(version) && (compareToVersion(version) >= 0); + return isComparable(version) && compareToVersion(version) >= 0; } @@ -238,7 +238,7 @@ public final boolean greaterEquals(final ProtocolVersion version) { * {@code false} otherwise */ public final boolean lessEquals(final ProtocolVersion version) { - return isComparable(version) && (compareToVersion(version) <= 0); + return isComparable(version) && compareToVersion(version) <= 0; } @Internal diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/config/NamedElementChain.java b/httpcore5/src/main/java/org/apache/hc/core5/http/config/NamedElementChain.java index 77f882af31..8fe70dcd37 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/config/NamedElementChain.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/config/NamedElementChain.java @@ -183,7 +183,7 @@ public Node getPrevious() { } public Node getNext() { - return next != master ? next: null; + return next != master ? next : null; } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/CharCodingSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/CharCodingSupport.java index f701e71f02..ff6d6a4176 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/CharCodingSupport.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/CharCodingSupport.java @@ -49,7 +49,7 @@ public static CharsetDecoder createDecoder(final CharCodingConfig cconfig) { if (charset != null) { return charset.newDecoder() .onMalformedInput(malformed != null ? malformed : CodingErrorAction.REPORT) - .onUnmappableCharacter(unmappable != null ? unmappable: CodingErrorAction.REPORT); + .onUnmappableCharacter(unmappable != null ? unmappable : CodingErrorAction.REPORT); } return null; } @@ -64,7 +64,7 @@ public static CharsetEncoder createEncoder(final CharCodingConfig cconfig) { final CodingErrorAction unmappable = cconfig.getUnmappableInputAction(); return charset.newEncoder() .onMalformedInput(malformed != null ? malformed : CodingErrorAction.REPORT) - .onUnmappableCharacter(unmappable != null ? unmappable: CodingErrorAction.REPORT); + .onUnmappableCharacter(unmappable != null ? unmappable : CodingErrorAction.REPORT); } return null; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/EnglishReasonPhraseCatalog.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/EnglishReasonPhraseCatalog.java index 7faf37e80d..69801b5fd7 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/EnglishReasonPhraseCatalog.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/EnglishReasonPhraseCatalog.java @@ -74,7 +74,7 @@ protected EnglishReasonPhraseCatalog() { public String getReason(final int status, final Locale loc) { Args.checkRange(status, 100, 599, "Unknown category for status code"); final int category = status / 100; - final int subcode = status - 100*category; + final int subcode = status - 100 * category; String reason = null; if (REASON_PHRASES[category].length > subcode) { @@ -106,7 +106,7 @@ public String getReason(final int status, final Locale loc) { */ private static void setReason(final int status, final String reason) { final int category = status / 100; - final int subcode = status - 100*category; + final int subcode = status - 100 * category; REASON_PHRASES[category][subcode] = reason; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java index 3da9d11dd7..db96adfdb5 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/HttpProcessors.java @@ -32,6 +32,7 @@ import org.apache.hc.core5.http.protocol.RequestConnControl; import org.apache.hc.core5.http.protocol.RequestContent; import org.apache.hc.core5.http.protocol.RequestExpectContinue; +import org.apache.hc.core5.http.protocol.RequestTE; import org.apache.hc.core5.http.protocol.RequestTargetHost; import org.apache.hc.core5.http.protocol.RequestUserAgent; import org.apache.hc.core5.http.protocol.RequestValidateHost; @@ -112,6 +113,44 @@ public static HttpProcessorBuilder customClient(final String agentInfo) { RequestExpectContinue.INSTANCE); } + /** + * Creates an {@link HttpProcessorBuilder} initialized with strict protocol interceptors + * for client-side HTTP/1.1 processing. + *

+ * This configuration enforces stricter validation and processing of client requests, + * ensuring compliance with the HTTP protocol. It includes interceptors for handling + * target hosts, content, connection controls, and TE header validation, among others. + * The user agent can be customized using the provided {@code agentInfo} parameter. + * + * @param agentInfo the user agent info to be included in the {@code User-Agent} header. + * If {@code null} or blank, a default value will be used. + * @return the {@link HttpProcessorBuilder} configured with strict client-side interceptors. + * @since 5.4 + */ + public static HttpProcessorBuilder strictClient(final String agentInfo) { + return HttpProcessorBuilder.create() + .addAll( + RequestTargetHost.INSTANCE, + RequestContent.INSTANCE, + RequestConnControl.INSTANCE, + RequestTE.INSTANCE, + new RequestUserAgent(!TextUtils.isBlank(agentInfo) ? agentInfo : + VersionInfo.getSoftwareInfo(SOFTWARE, "org.apache.hc.core5", HttpProcessors.class)), + RequestExpectContinue.INSTANCE); + } + + /** + * Creates {@link HttpProcessorBuilder} initialized with default protocol interceptors + * for client side HTTP/1.1 processing. + * + * @param agentInfo the agent info text or {@code null} for default. + * @return the processor builder. + * @since 5.4 + */ + public static HttpProcessorBuilder customClient(final String agentInfo, final boolean strict) { + return strict ? strictClient(agentInfo) : customClient(agentInfo); + } + /** * Creates {@link HttpProcessor} initialized with default protocol interceptors * for client side HTTP/1.1 processing. @@ -120,7 +159,7 @@ public static HttpProcessorBuilder customClient(final String agentInfo) { * @return the processor. */ public static HttpProcessor client(final String agentInfo) { - return customClient(agentInfo).build(); + return client(agentInfo, false); } /** @@ -130,7 +169,47 @@ public static HttpProcessor client(final String agentInfo) { * @return the processor. */ public static HttpProcessor client() { - return customClient(null).build(); + return client(null); + } + + /** + * Creates an {@link HttpProcessor} for client-side HTTP/2 processing. + * This method allows the option to include strict protocol interceptors. + * + * @param agentInfo the agent info text or {@code null} for default. + * @param strict if {@code true}, strict protocol interceptors will be added, including the {@code TE} header validation. + * @return the configured HTTP processor. + * @since 5.4 + */ + public static HttpProcessor client(final String agentInfo, final boolean strict) { + return customClient(agentInfo, strict).build(); + } + + /** + * Creates an {@link HttpProcessor} for client-side HTTP/2 processing + * with strict protocol validation interceptors by default. + *

+ * Strict validation includes additional checks such as validating the {@code TE} header. + * + * @return the configured strict HTTP processor. + * @since 5.4 + */ + public static HttpProcessor clientStrict() { + return customClient(null, true).build(); + } + + /** + * Creates an {@link HttpProcessor} for client-side HTTP/2 processing + * with strict protocol validation interceptors, using the specified agent information. + *

+ * Strict validation includes additional checks such as validating the {@code TE} header. + * + * @param agentInfo the agent info text or {@code null} for default. + * @return the configured strict HTTP processor. + * @since 5.4 + */ + public static HttpProcessor clientStrict(final String agentInfo) { + return customClient(agentInfo, true).build(); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/NoConnectionReuseStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/NoConnectionReuseStrategy.java new file mode 100644 index 0000000000..6e5e85c234 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/NoConnectionReuseStrategy.java @@ -0,0 +1,51 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http.impl; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.ConnectionReuseStrategy; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * A strategy that never reuses a connection. + * @since 5.4 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class NoConnectionReuseStrategy implements ConnectionReuseStrategy { + + public static final NoConnectionReuseStrategy INSTANCE = new NoConnectionReuseStrategy(); + + @Override + public boolean keepAlive(final HttpRequest request, final HttpResponse response, final HttpContext context) { + return false; + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequester.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequester.java index 2e91765c7e..9bfbdd8ba4 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequester.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequester.java @@ -47,10 +47,12 @@ import org.apache.hc.core5.reactor.DefaultConnectingIOReactor; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOReactorService; import org.apache.hc.core5.reactor.IOReactorStatus; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.IOWorkerSelector; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; @@ -73,7 +75,9 @@ public AsyncRequester( final Callback exceptionCallback, final IOSessionListener sessionListener, final Callback sessionShutdownCallback, - final Resolver addressResolver) { + final Resolver addressResolver, + final IOReactorMetricsListener threadPoolListener, + final IOWorkerSelector workerSelector) { this.ioReactor = new DefaultConnectingIOReactor( eventHandlerFactory, ioReactorConfig, @@ -81,7 +85,9 @@ public AsyncRequester( ioSessionDecorator, exceptionCallback, sessionListener, - sessionShutdownCallback); + threadPoolListener, + sessionShutdownCallback, + workerSelector); this.addressResolver = addressResolver != null ? addressResolver : DefaultAddressResolver.INSTANCE; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequesterBootstrap.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequesterBootstrap.java index 12611bf5ab..3f8c82ac32 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequesterBootstrap.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncRequesterBootstrap.java @@ -49,6 +49,7 @@ import org.apache.hc.core5.pool.StrictConnPool; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; import org.apache.hc.core5.util.Timeout; @@ -77,6 +78,7 @@ public class AsyncRequesterBootstrap { private IOSessionListener sessionListener; private Http1StreamListener streamListener; private ConnPoolListener connPoolListener; + private IOReactorMetricsListener threadPoolListener; private AsyncRequesterBootstrap() { } @@ -216,6 +218,17 @@ public final AsyncRequesterBootstrap setIOSessionListener(final IOSessionListene return this; } + /** + * Sets {@link IOReactorMetricsListener} instance. + * + * @return this instance. + * @since 5.4 + */ + public final AsyncRequesterBootstrap setIOReactorMetricsListener(final IOReactorMetricsListener threadPoolListener) { + this.threadPoolListener = threadPoolListener; + return this; + } + /** * Sets {@link Http1StreamListener} instance. * @@ -279,7 +292,9 @@ public HttpAsyncRequester create() { sessionListener, connPool, tlsStrategyCopy, - handshakeTimeout); + handshakeTimeout, + threadPoolListener, + null); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServer.java index 97b241d4c0..2b6fc99059 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServer.java @@ -43,10 +43,12 @@ import org.apache.hc.core5.reactor.DefaultListeningIOReactor; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOReactorService; import org.apache.hc.core5.reactor.IOReactorStatus; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.IOWorkerSelector; import org.apache.hc.core5.reactor.ListenerEndpoint; import org.apache.hc.core5.util.TimeValue; @@ -64,7 +66,9 @@ public AsyncServer( final Decorator ioSessionDecorator, final Callback exceptionCallback, final IOSessionListener sessionListener, - final Callback sessionShutdownCallback) { + final IOReactorMetricsListener threadPoolListener, + final Callback sessionShutdownCallback, + final IOWorkerSelector workerSelector) { this.ioReactor = new DefaultListeningIOReactor( eventHandlerFactory, ioReactorConfig, @@ -73,7 +77,9 @@ public AsyncServer( ioSessionDecorator, exceptionCallback, sessionListener, - sessionShutdownCallback); + threadPoolListener, + sessionShutdownCallback, + workerSelector); } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java index a47d041112..648a49acb0 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/AsyncServerBootstrap.java @@ -59,12 +59,12 @@ import org.apache.hc.core5.http.nio.support.DefaultAsyncResponseExchangeHandlerFactory; import org.apache.hc.core5.http.nio.support.TerminalAsyncServerFilter; import org.apache.hc.core5.http.protocol.HttpProcessor; -import org.apache.hc.core5.http.protocol.LookupRegistry; import org.apache.hc.core5.http.protocol.UriPatternType; import org.apache.hc.core5.net.InetAddressUtils; import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; import org.apache.hc.core5.util.Args; @@ -94,10 +94,13 @@ public class AsyncServerBootstrap { private Callback exceptionCallback; private IOSessionListener sessionListener; private Http1StreamListener streamListener; + private IOReactorMetricsListener threadPoolListener; + private boolean localAuthorityResolver; private AsyncServerBootstrap() { this.routeEntries = new ArrayList<>(); this.filters = new ArrayList<>(); + this.localAuthorityResolver = false; } /** @@ -208,6 +211,17 @@ public final AsyncServerBootstrap setIOSessionDecorator(final Decorator> lookupRegistry) { + public final AsyncServerBootstrap setLookupRegistry(final org.apache.hc.core5.http.protocol.LookupRegistry> lookupRegistry) { this.lookupRegistry = lookupRegistry; return this; } @@ -423,6 +437,16 @@ public final AsyncServerBootstrap addFilterLast(final String name, final AsyncFi return this; } + /** + * Create {@link RequestRouter} with LOCAL_AUTHORITY_RESOLVER (default: IGNORE_PORT_AUTHORITY_RESOLVER). + * + * @since 5.4 + */ + public final AsyncServerBootstrap enableLocalAuthorityResolver() { + this.localAuthorityResolver = true; + return this; + } + public HttpAsyncServer create() { final String actualCanonicalHostName = canonicalHostName != null ? canonicalHostName : InetAddressUtils.getCanonicalLocalHostName(); final HttpRequestMapper> requestRouterCopy; @@ -439,9 +463,9 @@ public HttpAsyncServer create() { requestRouterCopy = requestRouter; } else { requestRouterCopy = RequestRouter.create( - new URIAuthority(actualCanonicalHostName), + this.localAuthorityResolver ? RequestRouter.LOCAL_AUTHORITY : new URIAuthority(actualCanonicalHostName), UriPatternType.URI_PATTERN, routeEntries, - RequestRouter.IGNORE_PORT_AUTHORITY_RESOLVER, + this.localAuthorityResolver ? RequestRouter.LOCAL_AUTHORITY_RESOLVER : RequestRouter.IGNORE_PORT_AUTHORITY_RESOLVER, requestRouter); } } @@ -499,13 +523,14 @@ public HttpAsyncServer create() { new DefaultHttpResponseWriterFactory(http1Config), DefaultContentLengthStrategy.INSTANCE, DefaultContentLengthStrategy.INSTANCE, - streamListener); + streamListener, + exceptionCallback); final IOEventHandlerFactory ioEventHandlerFactory = new ServerHttp1IOEventHandlerFactory( streamHandlerFactory, tlsStrategy, handshakeTimeout); return new HttpAsyncServer(ioEventHandlerFactory, ioReactorConfig, ioSessionDecorator, exceptionCallback, - sessionListener); + sessionListener, threadPoolListener, null, null); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/FilterEntry.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/FilterEntry.java index 1adbbcf34b..a47b9d2255 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/FilterEntry.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/FilterEntry.java @@ -29,7 +29,7 @@ final class FilterEntry { - enum Position {BEFORE, AFTER, REPLACE, FIRST, LAST} + enum Position { BEFORE, AFTER, REPLACE, FIRST, LAST } final Position position; final String name; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java index bb2cbbeaf7..b94f051a6d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java @@ -37,9 +37,9 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.BasicFuture; import org.apache.hc.core5.concurrent.CallbackContribution; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.concurrent.ComplexFuture; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Decorator; import org.apache.hc.core5.http.ConnectionClosedException; @@ -78,8 +78,10 @@ import org.apache.hc.core5.reactor.IOEventHandler; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.IOWorkerSelector; import org.apache.hc.core5.reactor.ProtocolIOSession; import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer; import org.apache.hc.core5.util.Args; @@ -111,29 +113,17 @@ public HttpAsyncRequester( final IOSessionListener sessionListener, final ManagedConnPool connPool, final TlsStrategy tlsStrategy, - final Timeout handshakeTimeout) { + final Timeout handshakeTimeout, + final IOReactorMetricsListener threadPoolListener, + final IOWorkerSelector workerSelector) { super(eventHandlerFactory, ioReactorConfig, ioSessionDecorator, exceptionCallback, sessionListener, - ShutdownCommand.GRACEFUL_IMMEDIATE_CALLBACK, DefaultAddressResolver.INSTANCE); + ShutdownCommand.GRACEFUL_IMMEDIATE_CALLBACK, DefaultAddressResolver.INSTANCE, threadPoolListener, + workerSelector); this.connPool = Args.notNull(connPool, "Connection pool"); this.tlsStrategy = tlsStrategy; this.handshakeTimeout = handshakeTimeout; } - /** - * Use {@link AsyncRequesterBootstrap} to create instances of this class. - */ - @Internal - public HttpAsyncRequester( - final IOReactorConfig ioReactorConfig, - final IOEventHandlerFactory eventHandlerFactory, - final Decorator ioSessionDecorator, - final Callback exceptionCallback, - final IOSessionListener sessionListener, - final ManagedConnPool connPool) { - this(ioReactorConfig, eventHandlerFactory, ioSessionDecorator, exceptionCallback, sessionListener, connPool, - null, null); - } - @Override public PoolStats getTotalStats() { return connPool.getTotalStats(); @@ -289,7 +279,7 @@ public void execute( exchangeHandler.produceRequest((request, entityDetails, requestContext) -> { final HttpHost host = target != null ? target : defaultTarget(request); if (request.getAuthority() == null) { - request.setAuthority(new URIAuthority(host.getHostName(), host.getPort())); + request.setAuthority(new URIAuthority(host)); } connect(host, timeout, null, new FutureCallback() { @@ -417,14 +407,7 @@ public final Future execute( final AsyncClientExchangeHandler exchangeHandler = new BasicClientExchangeHandler<>( requestProducer, responseConsumer, - new FutureContribution(future) { - - @Override - public void completed(final T result) { - future.completed(result); - } - - }); + new CompletingFutureContribution(future)); execute(target, exchangeHandler, pushHandlerFactory, timeout, context != null ? context : HttpCoreContext.create()); return future; } @@ -538,7 +521,7 @@ public boolean isConnected() { return false; } final IOEventHandler handler = ioSession.getHandler(); - return (handler instanceof HttpConnection) && ((HttpConnection) handler).isOpen(); + return handler instanceof HttpConnection && ((HttpConnection) handler).isOpen(); } return false; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncServer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncServer.java index 32dde4c33a..04241385fd 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncServer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncServer.java @@ -35,12 +35,14 @@ import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Decorator; import org.apache.hc.core5.http.URIScheme; -import org.apache.hc.core5.reactor.EndpointParameters; import org.apache.hc.core5.http.nio.command.ShutdownCommand; +import org.apache.hc.core5.reactor.EndpointParameters; import org.apache.hc.core5.reactor.IOEventHandlerFactory; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.IOReactorMetricsListener; import org.apache.hc.core5.reactor.IOSession; import org.apache.hc.core5.reactor.IOSessionListener; +import org.apache.hc.core5.reactor.IOWorkerSelector; import org.apache.hc.core5.reactor.ListenerEndpoint; /** @@ -64,25 +66,14 @@ public HttpAsyncServer( final Decorator ioSessionDecorator, final Callback exceptionCallback, final IOSessionListener sessionListener, + final IOReactorMetricsListener threadPoolListener, + final IOWorkerSelector workerSelector, final String canonicalName) { super(eventHandlerFactory, ioReactorConfig, ioSessionDecorator, exceptionCallback, sessionListener, - ShutdownCommand.GRACEFUL_NORMAL_CALLBACK); + threadPoolListener, ShutdownCommand.GRACEFUL_NORMAL_CALLBACK, workerSelector); this.canonicalName = canonicalName; } - /** - * Use {@link AsyncServerBootstrap} to create instances of this class. - */ - @Internal - public HttpAsyncServer( - final IOEventHandlerFactory eventHandlerFactory, - final IOReactorConfig ioReactorConfig, - final Decorator ioSessionDecorator, - final Callback exceptionCallback, - final IOSessionListener sessionListener) { - this(eventHandlerFactory, ioReactorConfig, ioSessionDecorator, exceptionCallback, sessionListener, null); - } - /** * @since 5.1 */ diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java index bee1448e6e..57e110b1ee 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java @@ -45,6 +45,8 @@ import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import jdk.net.ExtendedSocketOptions; +import jdk.net.Sockets; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Resolver; @@ -76,13 +78,13 @@ import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.io.Closer; import org.apache.hc.core5.io.ModalCloseable; -import org.apache.hc.core5.io.SocketSupport; import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.pool.ConnPoolControl; import org.apache.hc.core5.pool.ManagedConnPool; import org.apache.hc.core5.pool.PoolEntry; import org.apache.hc.core5.pool.PoolStats; import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.ReflectionUtils; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; @@ -239,6 +241,7 @@ public T execute( } } + @SuppressWarnings("Since15") private HttpClientConnection createConnection(final Socket sock, final HttpHost targetHost) throws IOException { sock.setSoTimeout(socketConfig.getSoTimeout().toMillisecondsIntBound()); sock.setReuseAddress(socketConfig.isSoReuseAddress()); @@ -250,14 +253,16 @@ private HttpClientConnection createConnection(final Socket sock, final HttpHost if (socketConfig.getSndBufSize() > 0) { sock.setSendBufferSize(socketConfig.getSndBufSize()); } - if (this.socketConfig.getTcpKeepIdle() > 0) { - SocketSupport.setOption(sock, SocketSupport.TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle()); - } - if (this.socketConfig.getTcpKeepInterval() > 0) { - SocketSupport.setOption(sock, SocketSupport.TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval()); - } - if (this.socketConfig.getTcpKeepCount() > 0) { - SocketSupport.setOption(sock, SocketSupport.TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount()); + if (ReflectionUtils.supportsKeepAliveOptions()) { + if (this.socketConfig.getTcpKeepIdle() > 0) { + Sockets.setOption(sock, ExtendedSocketOptions.TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle()); + } + if (this.socketConfig.getTcpKeepInterval() > 0) { + Sockets.setOption(sock, ExtendedSocketOptions.TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval()); + } + if (this.socketConfig.getTcpKeepCount() > 0) { + Sockets.setOption(sock, ExtendedSocketOptions.TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount()); + } } final int linger = socketConfig.getSoLinger().toMillisecondsIntBound(); if (linger >= 0) { @@ -332,7 +337,7 @@ public ClassicHttpResponse execute( } } if (request.getAuthority() == null) { - request.setAuthority(new URIAuthority(targetHost.getHostName(), targetHost.getPort())); + request.setAuthority(new URIAuthority(targetHost)); } final ClassicHttpResponse response = execute(connection, request, informationCallback, context); final HttpEntity entity = response.getEntity(); @@ -429,7 +434,7 @@ public ClassicHttpResponse execute( return execute(targetHost, request, null, connectTimeout, context); } - public T execute( + public T execute( final HttpHost targetHost, final ClassicHttpRequest request, final Timeout connectTimeout, diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java index 5a8deaa2d7..8a67d4ec3f 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java @@ -41,6 +41,8 @@ import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; +import jdk.net.ExtendedSocketOptions; +import jdk.net.Sockets; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.DefaultThreadFactory; import org.apache.hc.core5.function.Callback; @@ -56,8 +58,8 @@ import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.io.Closer; import org.apache.hc.core5.io.ModalCloseable; -import org.apache.hc.core5.io.SocketSupport; import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.ReflectionUtils; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; @@ -138,6 +140,7 @@ public int getLocalPort() { return -1; } + @SuppressWarnings("Since15") public void start() throws IOException { if (this.status.compareAndSet(Status.READY, Status.ACTIVE)) { this.serverSocket = this.serverSocketFactory.createServerSocket( @@ -146,14 +149,16 @@ public void start() throws IOException { if (this.socketConfig.getRcvBufSize() > 0) { this.serverSocket.setReceiveBufferSize(this.socketConfig.getRcvBufSize()); } - if (this.socketConfig.getTcpKeepIdle() > 0) { - SocketSupport.setOption(this.serverSocket, SocketSupport.TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle()); - } - if (this.socketConfig.getTcpKeepInterval() > 0) { - SocketSupport.setOption(this.serverSocket, SocketSupport.TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval()); - } - if (this.socketConfig.getTcpKeepCount() > 0) { - SocketSupport.setOption(this.serverSocket, SocketSupport.TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount()); + if (ReflectionUtils.supportsKeepAliveOptions()) { + if (this.socketConfig.getTcpKeepIdle() > 0) { + Sockets.setOption(this.serverSocket, ExtendedSocketOptions.TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle()); + } + if (this.socketConfig.getTcpKeepInterval() > 0) { + Sockets.setOption(this.serverSocket, ExtendedSocketOptions.TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval()); + } + if (this.socketConfig.getTcpKeepCount() > 0) { + Sockets.setOption(this.serverSocket, ExtendedSocketOptions.TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount()); + } } if (this.sslSetupHandler != null && this.serverSocket instanceof SSLServerSocket) { final SSLServerSocket sslServerSocket = (SSLServerSocket) this.serverSocket; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java index 1c58748a94..9d06771976 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java @@ -38,6 +38,8 @@ import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import jdk.net.ExtendedSocketOptions; +import jdk.net.Sockets; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ExceptionListener; import org.apache.hc.core5.http.impl.io.HttpService; @@ -45,7 +47,7 @@ import org.apache.hc.core5.http.io.HttpServerConnection; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.io.Closer; -import org.apache.hc.core5.io.SocketSupport; +import org.apache.hc.core5.util.ReflectionUtils; class RequestListener implements Runnable { @@ -79,6 +81,7 @@ public RequestListener( this.terminated = new AtomicBoolean(); } + @SuppressWarnings("Since15") private HttpServerConnection createConnection(final Socket socket) throws IOException { socket.setSoTimeout(this.socketConfig.getSoTimeout().toMillisecondsIntBound()); socket.setKeepAlive(this.socketConfig.isSoKeepAlive()); @@ -92,14 +95,16 @@ private HttpServerConnection createConnection(final Socket socket) throws IOExce if (this.socketConfig.getSoLinger().toSeconds() >= 0) { socket.setSoLinger(true, this.socketConfig.getSoLinger().toSecondsIntBound()); } - if (this.socketConfig.getTcpKeepIdle() > 0) { - SocketSupport.setOption(this.serverSocket, SocketSupport.TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle()); - } - if (this.socketConfig.getTcpKeepInterval() > 0) { - SocketSupport.setOption(this.serverSocket, SocketSupport.TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval()); - } - if (this.socketConfig.getTcpKeepCount() > 0) { - SocketSupport.setOption(this.serverSocket, SocketSupport.TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount()); + if (ReflectionUtils.supportsKeepAliveOptions()) { + if (this.socketConfig.getTcpKeepIdle() > 0) { + Sockets.setOption(this.serverSocket, ExtendedSocketOptions.TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle()); + } + if (this.socketConfig.getTcpKeepInterval() > 0) { + Sockets.setOption(this.serverSocket, ExtendedSocketOptions.TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval()); + } + if (this.socketConfig.getTcpKeepCount() > 0) { + Sockets.setOption(this.serverSocket, ExtendedSocketOptions.TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount()); + } } if (!(socket instanceof SSLSocket) && sslSocketFactory != null) { final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, null, -1, false); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java index da887dbf1c..86a9135cfa 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/ServerBootstrap.java @@ -98,10 +98,12 @@ public class ServerBootstrap { private HttpConnectionFactory connectionFactory; private ExceptionListener exceptionListener; private Http1StreamListener streamListener; + private boolean localAuthorityResolver; private ServerBootstrap() { this.routeEntries = new ArrayList<>(); this.filters = new ArrayList<>(); + this.localAuthorityResolver = false; } public static ServerBootstrap bootstrap() { @@ -346,6 +348,16 @@ public final ServerBootstrap addFilterLast(final String name, final HttpFilterHa return this; } + /** + * Create {@link RequestRouter} with LOCAL_AUTHORITY_RESOLVER (default: IGNORE_PORT_AUTHORITY_RESOLVER). + * + * @since 5.4 + */ + public final ServerBootstrap enableLocalAuthorityResolver() { + this.localAuthorityResolver = true; + return this; + } + public HttpServer create() { final String actualCanonicalHostName = canonicalHostName != null ? canonicalHostName : InetAddressUtils.getCanonicalLocalHostName(); final HttpRequestMapper requestRouterCopy; @@ -362,10 +374,10 @@ public HttpServer create() { requestRouterCopy = requestRouter; } else { requestRouterCopy = RequestRouter.create( - new URIAuthority(actualCanonicalHostName), + this.localAuthorityResolver ? RequestRouter.LOCAL_AUTHORITY : new URIAuthority(actualCanonicalHostName), UriPatternType.URI_PATTERN, routeEntries, - RequestRouter.IGNORE_PORT_AUTHORITY_RESOLVER, + this.localAuthorityResolver ? RequestRouter.LOCAL_AUTHORITY_RESOLVER : RequestRouter.IGNORE_PORT_AUTHORITY_RESOLVER, requestRouter); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/AbstractMessageParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/AbstractMessageParser.java index cf5e40d48b..110ea8e70d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/AbstractMessageParser.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/AbstractMessageParser.java @@ -55,8 +55,8 @@ */ public abstract class AbstractMessageParser implements HttpMessageParser { - private static final int HEAD_LINE = 0; - private static final int HEADERS = 1; + private static final int HEAD_LINE = 0; + private static final int HEADERS = 1; private final Http1Config http1Config; private final List headerLines; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java index 8e88f22d99..a300580379 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java @@ -48,13 +48,13 @@ import org.apache.hc.core5.http.EndpointDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpMessage; import org.apache.hc.core5.http.ProtocolVersion; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.impl.BasicEndpointDetails; import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics; +import org.apache.hc.core5.http.impl.io.support.IncomingHttpEntity; import org.apache.hc.core5.http.io.BHttpConnection; import org.apache.hc.core5.http.io.SessionInputBuffer; import org.apache.hc.core5.http.io.SessionOutputBuffer; @@ -186,9 +186,8 @@ HttpEntity createIncomingEntity( final long len) { return new IncomingHttpEntity( createContentInputStream(len, inBuffer, inputStream), - len >= 0 ? len : -1, len == ContentLengthStrategy.CHUNKED, - message.getFirstHeader(HttpHeaders.CONTENT_TYPE), - message.getFirstHeader(HttpHeaders.CONTENT_ENCODING)); + len, + message); } @Override @@ -248,11 +247,17 @@ public void close(final CloseMode closeMode) { try { if (sslSocket != null) { try { - if (!sslSocket.isOutputShutdown()) { - sslSocket.shutdownOutput(); - } - if (!sslSocket.isInputShutdown()) { - sslSocket.shutdownInput(); + try { + if (!sslSocket.isOutputShutdown()) { + sslSocket.shutdownOutput(); + } + if (!sslSocket.isInputShutdown()) { + sslSocket.shutdownInput(); + } + } catch (final UnsupportedOperationException ignore) { + // Some JREs such as Oracle JDK 1.8 do not support + // Socket#shutdownOutput() and Socket#shutdownInput() methods + // "because of the TLS half-close policy" } sslSocket.close(); } catch (final IOException ignore) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java index 2f4ab30e83..a2b9495ee2 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java @@ -168,7 +168,8 @@ public static final class Builder { private HttpMessageWriterFactory requestWriterFactory; private HttpMessageParserFactory responseParserFactory; - private Builder() {} + private Builder() { + } public Builder http1Config(final Http1Config http1Config) { this.http1Config = http1Config; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java index 4b84958dd5..e89b425287 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java @@ -149,7 +149,8 @@ public static final class Builder { private HttpMessageParserFactory requestParserFactory; private HttpMessageWriterFactory responseWriterFactory; - private Builder() {} + private Builder() { + } public Builder scheme(final String scheme) { this.scheme = scheme; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpRequestExecutor.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpRequestExecutor.java index e97f79fa9c..9bff2058ef 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpRequestExecutor.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpRequestExecutor.java @@ -54,10 +54,13 @@ import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.io.Closer; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.Timeout; +import javax.net.ssl.SSLException; + /** * {@code HttpRequestExecutor} is a client side HTTP protocol handler based * on the blocking (classic) I/O model. @@ -179,7 +182,7 @@ public ClassicHttpResponse execute( } response = null; continue; - } else if (status >= HttpStatus.SC_CLIENT_ERROR){ + } else if (status >= HttpStatus.SC_CLIENT_ERROR) { conn.terminateRequest(request); } else { conn.sendRequestEntity(request); @@ -211,9 +214,12 @@ public ClassicHttpResponse execute( } return response; - } catch (final HttpException | IOException | RuntimeException ex) { + } catch (final HttpException | SSLException ex) { Closer.closeQuietly(conn); throw ex; + } catch (final IOException | RuntimeException ex) { + Closer.close(conn, CloseMode.IMMEDIATE); + throw ex; } } @@ -347,7 +353,8 @@ public static final class Builder { private ConnectionReuseStrategy connReuseStrategy; private Http1StreamListener streamListener; - private Builder() {} + private Builder() { + } public Builder withWaitForContinue(final Timeout waitForContinue) { this.waitForContinue = waitForContinue; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpService.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpService.java index d21f69b935..d92798ce59 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpService.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/HttpService.java @@ -165,8 +165,8 @@ public HttpService( final ConnectionReuseStrategy connReuseStrategy, final Http1StreamListener streamListener) { super(); - this.processor = Args.notNull(processor, "HTTP processor"); - this.requestHandler = Args.notNull(requestHandler, "Request handler"); + this.processor = Args.notNull(processor, "HTTP processor"); + this.requestHandler = Args.notNull(requestHandler, "Request handler"); this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT; this.connReuseStrategy = connReuseStrategy != null ? connReuseStrategy : DefaultConnectionReuseStrategy.INSTANCE; this.streamListener = streamListener; @@ -334,7 +334,8 @@ public static final class Builder { private ConnectionReuseStrategy connReuseStrategy; private Http1StreamListener streamListener; - private Builder() {} + private Builder() { + } public Builder withHttpProcessor(final HttpProcessor processor) { this.processor = processor; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/SessionInputBufferImpl.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/SessionInputBufferImpl.java index a0862b37d0..9f52527810 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/SessionInputBufferImpl.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/SessionInputBufferImpl.java @@ -319,7 +319,7 @@ private int lineFromLineBuffer(final CharArrayBuffer charBuffer) if (this.decoder == null) { charBuffer.append(this.lineBuffer, 0, len); } else { - final ByteBuffer bbuf = ByteBuffer.wrap(this.lineBuffer.array(), 0, len); + final ByteBuffer bbuf = ByteBuffer.wrap(this.lineBuffer.array(), 0, len); len = appendDecoded(charBuffer, bbuf); } this.lineBuffer.clear(); @@ -340,7 +340,7 @@ private int lineFromReadBuffer(final CharArrayBuffer charbuffer, final int posit if (this.decoder == null) { charbuffer.append(this.buffer, off, len); } else { - final ByteBuffer bbuf = ByteBuffer.wrap(this.buffer, off, len); + final ByteBuffer bbuf = ByteBuffer.wrap(this.buffer, off, len); len = appendDecoded(charbuffer, bbuf); } return len; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/support/IncomingHttpEntity.java similarity index 80% rename from httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java rename to httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/support/IncomingHttpEntity.java index 91edff19f8..06f7baad85 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/support/IncomingHttpEntity.java @@ -25,7 +25,7 @@ * */ -package org.apache.hc.core5.http.impl.io; +package org.apache.hc.core5.http.impl.io.support; import java.io.IOException; import java.io.InputStream; @@ -34,26 +34,27 @@ import java.util.List; import java.util.Set; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentLengthStrategy; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpMessage; import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; import org.apache.hc.core5.io.Closer; -class IncomingHttpEntity implements HttpEntity { +@Internal +public class IncomingHttpEntity implements HttpEntity { private final InputStream content; private final long len; - private final boolean chunked; - private final Header contentType; - private final Header contentEncoding; + private final HttpMessage message; - IncomingHttpEntity(final InputStream content, final long len, final boolean chunked, final Header contentType, final Header contentEncoding) { + public IncomingHttpEntity(final InputStream content, final long len, final HttpMessage message) { this.content = content; this.len = len; - this.chunked = chunked; - this.contentType = contentType; - this.contentEncoding = contentEncoding; + this.message = message; } @Override @@ -63,22 +64,24 @@ public boolean isRepeatable() { @Override public boolean isChunked() { - return chunked; + return len == ContentLengthStrategy.CHUNKED; } @Override public long getContentLength() { - return len; + return len >= 0 ? len : -1; } @Override public String getContentType() { - return contentType != null ? contentType.getValue() : null; + final Header h = message.getFirstHeader(HttpHeaders.CONTENT_TYPE); + return h != null ? h.getValue() : null; } @Override public String getContentEncoding() { - return contentEncoding != null ? contentEncoding.getValue() : null; + final Header h = message.getFirstHeader(HttpHeaders.CONTENT_ENCODING); + return h != null ? h.getValue() : null; } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractHttp1StreamDuplexer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractHttp1StreamDuplexer.java index b5dfbdc75b..df23eb00c6 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractHttp1StreamDuplexer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractHttp1StreamDuplexer.java @@ -82,7 +82,7 @@ abstract class AbstractHttp1StreamDuplexer implements Identifiable, HttpConnection { - private enum ConnectionState { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN} + private enum ConnectionState { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN } private final ProtocolIOSession ioSession; private final Http1Config http1Config; @@ -212,6 +212,8 @@ abstract ContentEncoder createContentEncoder( abstract boolean isOutputReady(); + abstract boolean isRequestInitiated(); + abstract void produceOutput() throws HttpException, IOException; abstract void execute(RequestExecutionCommand executionCommand) throws HttpException, IOException; @@ -270,23 +272,22 @@ public final void onInput(final ByteBuffer src) throws HttpException, IOExceptio inTransportMetrics.incrementBytesTransferred(n); } - if (connState.compareTo(ConnectionState.GRACEFUL_SHUTDOWN) >= 0 && inbuf.hasData() && inputIdle()) { + if (connState.compareTo(ConnectionState.GRACEFUL_SHUTDOWN) >= 0 && !inbuf.hasData() && inputIdle()) { ioSession.clearEvent(SelectionKey.OP_READ); return; } boolean endOfStream = false; - if (incomingMessage == null) { - final int bytesRead = inbuf.fill(ioSession); - if (bytesRead > 0) { - inTransportMetrics.incrementBytesTransferred(bytesRead); - } - endOfStream = bytesRead == -1; - } do { if (incomingMessage == null) { + final int bytesRead = inbuf.fill(ioSession); + if (bytesRead > 0) { + inTransportMetrics.incrementBytesTransferred(bytesRead); + } + endOfStream = bytesRead == -1; + final IncomingMessage messageHead = parseMessageHead(endOfStream); if (messageHead != null) { this.version = messageHead.getVersion(); @@ -347,7 +348,7 @@ public final void onInput(final ByteBuffer src) throws HttpException, IOExceptio } while (inbuf.hasData()); if (endOfStream && !inbuf.hasData()) { - if (outputIdle() && inputIdle()) { + if (inputIdle()) { requestShutdown(CloseMode.GRACEFUL); } else { shutdownSession(new ConnectionClosedException("Connection closed by peer")); @@ -379,7 +380,7 @@ public final void onOutput() throws IOException, HttpException { } else { outputRequests.addAndGet(-pendingOutputRequests); } - outputEnd = outgoingMessage == null && !outbuf.hasData(); + outputEnd = outgoingMessage == null && !outbuf.hasData() && !isRequestInitiated(); } finally { ioSession.getLock().unlock(); } @@ -514,7 +515,7 @@ int streamOutput(final ByteBuffer src) throws IOException { } } - enum MessageDelineation { NONE, CHUNK_CODED, MESSAGE_HEAD} + enum MessageDelineation { NONE, CHUNK_CODED, MESSAGE_HEAD } MessageDelineation endOutputStream(final List trailers) throws IOException { ioSession.getLock().lock(); @@ -559,7 +560,7 @@ public void close(final CloseMode closeMode) { @Override public boolean isOpen() { - return connState == ConnectionState.ACTIVE; + return connState.compareTo(ConnectionState.ACTIVE) <= 0; } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractMessageParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractMessageParser.java index 1582da3721..3ef56e9afd 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractMessageParser.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/AbstractMessageParser.java @@ -66,6 +66,10 @@ private enum State { private int emptyLineCount; /** + * Constructs a new instance for a subclass. + * + * @param http1Config HTTP/1.1 protocol parameters. + * @param lineParser How to parse lines in an HTTP message. * @since 5.3 */ public AbstractMessageParser(final Http1Config http1Config, final LineParser lineParser) { @@ -77,6 +81,10 @@ public AbstractMessageParser(final Http1Config http1Config, final LineParser lin } /** + * Constructs a new instance for a subclass. + * + * @param lineParser How to parse lines in an HTTP message. + * @param messageConstraints HTTP/1.1 protocol parameters. * @deprecated Use {@link #AbstractMessageParser(Http1Config, LineParser)} */ @Deprecated @@ -147,7 +155,7 @@ private void parseHeader() throws IOException { public T parse( final SessionInputBuffer sessionBuffer, final boolean endOfStream) throws IOException, HttpException { Args.notNull(sessionBuffer, "Session input buffer"); - while (this.state !=State.COMPLETED) { + while (this.state != State.COMPLETED) { if (this.lineBuf == null) { this.lineBuf = new CharArrayBuffer(64); } else { @@ -157,7 +165,7 @@ public T parse( final int maxLineLen = this.http1Config.getMaxLineLength(); if (maxLineLen > 0 && (this.lineBuf.length() > maxLineLen || - (!lineComplete && sessionBuffer.length() > maxLineLen))) { + !lineComplete && sessionBuffer.length() > maxLineLen)) { throw new MessageConstraintException("Maximum line length limit exceeded"); } if (!lineComplete) { @@ -188,7 +196,7 @@ public T parse( this.state = State.COMPLETED; } } - if (this.state ==State. COMPLETED) { + if (this.state == State.COMPLETED) { for (final CharArrayBuffer buffer : this.headerBufs) { this.message.addHeader(this.lineParser.parseHeader(buffer)); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ChunkDecoder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ChunkDecoder.java index 58c32a7878..84a8c9a4f0 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ChunkDecoder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ChunkDecoder.java @@ -119,7 +119,7 @@ private void readChunkHead() throws IOException { final int maxLineLen = this.http1Config.getMaxLineLength(); if (maxLineLen > 0 && (this.lineBuf.length() > maxLineLen || - (!lineComplete && this.buffer.length() > maxLineLen))) { + !lineComplete && this.buffer.length() > maxLineLen)) { throw new MessageConstraintException("Maximum line length limit exceeded"); } if (lineComplete) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexer.java index 7299d7251d..83eaa2f1cc 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexer.java @@ -116,6 +116,11 @@ public void close() { shutdownSession(CloseMode.IMMEDIATE); } + @Override + public void close(final CloseMode closeMode) { + shutdownSession(closeMode); + } + @Override public void submit( final HttpRequest request, @@ -286,14 +291,19 @@ protected ContentEncoder createContentEncoder( } } + @Override + boolean isRequestInitiated() { + return outgoing != null && !outgoing.isRequestFinal(); + } + @Override boolean inputIdle() { - return incoming == null; + return incoming == null && pipeline.isEmpty(); } @Override boolean outputIdle() { - return outgoing == null && pipeline.isEmpty(); + return outgoing == null; } @Override @@ -386,7 +396,6 @@ boolean handleTimeout() { @Override void appendState(final StringBuilder buf) { - super.appendState(buf); super.appendState(buf); buf.append(", incoming=["); if (incoming != null) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexerFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexerFactory.java index 54691487e2..ce6b2b1023 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexerFactory.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamDuplexerFactory.java @@ -76,7 +76,7 @@ public ClientHttp1StreamDuplexerFactory( final Http1StreamListener streamListener) { this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT; - this.charCodingConfig = charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT; + this.charCodingConfig = charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT; this.connectionReuseStrategy = connectionReuseStrategy != null ? connectionReuseStrategy : DefaultConnectionReuseStrategy.INSTANCE; this.responseParserFactory = responseParserFactory != null ? responseParserFactory : diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamHandler.java index 62dd4c242e..b54c603dbf 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ClientHttp1StreamHandler.java @@ -30,6 +30,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.EntityDetails; @@ -65,14 +66,14 @@ class ClientHttp1StreamHandler implements ResourceHolder { private final ConnectionReuseStrategy connectionReuseStrategy; private final AsyncClientExchangeHandler exchangeHandler; private final HttpCoreContext context; + private final AtomicReference requestState; + private final AtomicReference responseState; private final AtomicBoolean requestCommitted; private final AtomicBoolean done; private volatile boolean keepAlive; private volatile Timeout timeout; private volatile HttpRequest committedRequest; - private volatile MessageState requestState; - private volatile MessageState responseState; ClientHttp1StreamHandler( final Http1StreamChannel outputChannel, @@ -91,8 +92,11 @@ public void requestOutput() { @Override public void endStream(final List trailers) throws IOException { + requestState.set(MessageState.COMPLETE); outputChannel.complete(trailers); - requestState = MessageState.COMPLETE; + if (!keepAlive && responseState.get() == MessageState.COMPLETE) { + outputChannel.close(); + } } @Override @@ -112,19 +116,23 @@ public void endStream() throws IOException { this.connectionReuseStrategy = connectionReuseStrategy; this.exchangeHandler = exchangeHandler; this.context = context; + this.requestState = new AtomicReference<>(MessageState.IDLE); + this.responseState = new AtomicReference<>(MessageState.HEADERS); this.requestCommitted = new AtomicBoolean(); this.done = new AtomicBoolean(); this.keepAlive = true; - this.requestState = MessageState.IDLE; - this.responseState = MessageState.HEADERS; + } + + boolean isRequestFinal() { + return requestState.get() == MessageState.COMPLETE; } boolean isResponseFinal() { - return responseState == MessageState.COMPLETE; + return responseState.get() == MessageState.COMPLETE; } boolean isCompleted() { - return requestState == MessageState.COMPLETE && responseState == MessageState.COMPLETE; + return requestState.get() == MessageState.COMPLETE && responseState.get() == MessageState.COMPLETE; } String getRequestMethod() { @@ -132,10 +140,12 @@ String getRequestMethod() { } boolean isOutputReady() { - switch (requestState) { + switch (requestState.get()) { case IDLE: - case ACK: return true; + case HEADERS: + case ACK: + return false; case BODY: return exchangeHandler.available() > 0; default: @@ -156,22 +166,24 @@ private void commitRequest(final HttpRequest request, final EntityDetails entity final boolean endStream = entityDetails == null; if (endStream) { - outputChannel.submit(request, true, FlushMode.IMMEDIATE); committedRequest = request; - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); + outputChannel.submit(request, true, FlushMode.IMMEDIATE); } else { final Header h = request.getFirstHeader(HttpHeaders.EXPECT); final boolean expectContinue = h != null && HeaderElements.CONTINUE.equalsIgnoreCase(h.getValue()); outputChannel.submit(request, false, expectContinue ? FlushMode.IMMEDIATE : FlushMode.BUFFER); committedRequest = request; if (expectContinue) { - requestState = MessageState.ACK; + requestState.set(MessageState.ACK); timeout = outputChannel.getSocketTimeout(); final Timeout timeout = http1Config.getWaitForContinueTimeout() != null ? http1Config.getWaitForContinueTimeout() : DEFAULT_WAIT_FOR_CONTINUE; outputChannel.setSocketTimeout(timeout); } else { - requestState = MessageState.BODY; exchangeHandler.produce(internalDataChannel); + if (requestState.compareAndSet(MessageState.HEADERS, MessageState.BODY)) { + outputChannel.requestOutput(); + } } } } else { @@ -180,9 +192,10 @@ private void commitRequest(final HttpRequest request, final EntityDetails entity } void produceOutput() throws HttpException, IOException { - switch (requestState) { + switch (requestState.get()) { case IDLE: - requestState = MessageState.HEADERS; + requestState.set(MessageState.HEADERS); + outputChannel.suspendOutput(); exchangeHandler.produceRequest((request, entityDetails, httpContext) -> commitRequest(request, entityDetails), context); break; case ACK: @@ -195,7 +208,7 @@ void produceOutput() throws HttpException, IOException { } void consumeHeader(final HttpResponse response, final EntityDetails entityDetails) throws HttpException, IOException { - if (done.get() || responseState != MessageState.HEADERS) { + if (done.get() || responseState.get() != MessageState.HEADERS) { throw new ProtocolException("Unexpected message head"); } final ProtocolVersion transportVersion = response.getVersion(); @@ -217,10 +230,10 @@ void consumeHeader(final HttpResponse response, final EntityDetails entityDetail keepAlive = false; } } - if (requestState == MessageState.ACK) { + if (requestState.get() == MessageState.ACK) { if (status == HttpStatus.SC_CONTINUE || status >= HttpStatus.SC_SUCCESS) { outputChannel.setSocketTimeout(timeout); - requestState = MessageState.BODY; + requestState.set(MessageState.BODY); if (status < HttpStatus.SC_CLIENT_ERROR) { exchangeHandler.produce(internalDataChannel); } @@ -229,9 +242,9 @@ void consumeHeader(final HttpResponse response, final EntityDetails entityDetail if (status < HttpStatus.SC_SUCCESS) { return; } - if (requestState == MessageState.BODY) { + if (requestState.get() == MessageState.BODY) { if (status >= HttpStatus.SC_CLIENT_ERROR) { - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); if (!outputChannel.abortGracefully()) { keepAlive = false; } @@ -247,15 +260,11 @@ void consumeHeader(final HttpResponse response, final EntityDetails entityDetail } exchangeHandler.consumeResponse(response, entityDetails, context); - if (entityDetails == null) { - responseState = MessageState.COMPLETE; - } else { - responseState = MessageState.BODY; - } + responseState.set(entityDetails == null ? MessageState.COMPLETE : MessageState.BODY); } void consumeData(final ByteBuffer src) throws HttpException, IOException { - if (done.get() || responseState != MessageState.BODY) { + if (done.get() || responseState.get() != MessageState.BODY) { throw new ProtocolException("Unexpected message data"); } exchangeHandler.consume(src); @@ -266,19 +275,19 @@ void updateCapacity(final CapacityChannel capacityChannel) throws IOException { } void dataEnd(final List trailers) throws HttpException, IOException { - if (done.get() || responseState != MessageState.BODY) { + if (done.get() || responseState.get() != MessageState.BODY) { throw new ProtocolException("Unexpected message data"); } - if (!keepAlive) { + responseState.set(MessageState.COMPLETE); + if (!keepAlive && requestState.get() == MessageState.COMPLETE) { outputChannel.close(); } - responseState = MessageState.COMPLETE; exchangeHandler.streamEnd(trailers); } boolean handleTimeout() { - if (requestState == MessageState.ACK) { - requestState = MessageState.BODY; + if (requestState.get() == MessageState.ACK) { + requestState.set(MessageState.BODY); outputChannel.setSocketTimeout(timeout); outputChannel.requestOutput(); return true; @@ -295,16 +304,16 @@ void failed(final Exception cause) { @Override public void releaseResources() { if (done.compareAndSet(false, true)) { - responseState = MessageState.COMPLETE; - requestState = MessageState.COMPLETE; + responseState.set(MessageState.COMPLETE); + requestState.set(MessageState.COMPLETE); exchangeHandler.releaseResources(); } } void appendState(final StringBuilder buf) { - buf.append("requestState=").append(requestState) - .append(", responseState=").append(responseState) - .append(", responseCommitted=").append(requestCommitted) + buf.append("requestState=").append(requestState.get()) + .append(", responseState=").append(responseState.get()) + .append(", requestCommitted=").append(requestCommitted) .append(", keepAlive=").append(keepAlive) .append(", done=").append(done); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/Http1StreamChannel.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/Http1StreamChannel.java index 3d4bd88318..6f4c9b3ef4 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/Http1StreamChannel.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/Http1StreamChannel.java @@ -31,12 +31,15 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpMessage; import org.apache.hc.core5.http.nio.ContentEncoder; +import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.util.Timeout; interface Http1StreamChannel extends ContentEncoder { void close(); + void close(CloseMode closeMode); + void activate() throws HttpException, IOException; void submit(OutgoingMessage messageHead, boolean endStream, final FlushMode flushMode) throws HttpException, IOException; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/LengthDelimitedDecoder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/LengthDelimitedDecoder.java index 1a45075037..2cc45b4879 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/LengthDelimitedDecoder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/LengthDelimitedDecoder.java @@ -73,7 +73,7 @@ public int read(final ByteBuffer dst) throws IOException { if (isCompleted()) { return -1; } - final int chunk = (int) Math.min((this.contentLength - this.len), Integer.MAX_VALUE); + final int chunk = (int) Math.min(this.contentLength - this.len, Integer.MAX_VALUE); final int bytesRead; if (this.buffer.hasData()) { @@ -113,7 +113,7 @@ public long transfer( return -1; } - final int chunk = (int) Math.min((this.contentLength - this.len), Integer.MAX_VALUE); + final int chunk = (int) Math.min(this.contentLength - this.len, Integer.MAX_VALUE); final long bytesRead; if (this.buffer.hasData()) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexer.java index 4dc3bf4c05..e4b1b6d6e3 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexer.java @@ -37,6 +37,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.ContentLengthStrategy; @@ -87,6 +88,7 @@ public class ServerHttp1StreamDuplexer extends AbstractHttp1StreamDuplexer exceptionCallback; private final Queue pipeline; private final Http1StreamChannel outputChannel; @@ -105,7 +107,8 @@ public ServerHttp1StreamDuplexer( final NHttpMessageWriter outgoingMessageWriter, final ContentLengthStrategy incomingContentStrategy, final ContentLengthStrategy outgoingContentStrategy, - final Http1StreamListener streamListener) { + final Http1StreamListener streamListener, + final Callback exceptionCallback) { super(ioSession, http1Config, charCodingConfig, incomingMessageParser, outgoingMessageWriter, incomingContentStrategy, outgoingContentStrategy); this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); @@ -115,6 +118,7 @@ public ServerHttp1StreamDuplexer( this.connectionReuseStrategy = connectionReuseStrategy != null ? connectionReuseStrategy : DefaultConnectionReuseStrategy.INSTANCE; this.streamListener = streamListener; + this.exceptionCallback = exceptionCallback; this.pipeline = new ConcurrentLinkedQueue<>(); this.outputChannel = new Http1StreamChannel() { @@ -123,6 +127,11 @@ public void close() { ServerHttp1StreamDuplexer.this.close(CloseMode.GRACEFUL); } + @Override + public void close(final CloseMode closeMode) { + ServerHttp1StreamDuplexer.this.close(closeMode); + } + @Override public void submit( final HttpResponse response, @@ -324,6 +333,7 @@ void terminateExchange(final HttpException ex) throws HttpException, IOException http1Config, connectionReuseStrategy, exchangeHandlerFactory, + exceptionCallback, context); outgoing = streamHandler; } else { @@ -333,6 +343,7 @@ void terminateExchange(final HttpException ex) throws HttpException, IOException http1Config, connectionReuseStrategy, exchangeHandlerFactory, + exceptionCallback, context); pipeline.add(streamHandler); } @@ -356,6 +367,7 @@ void consumeHeader(final HttpRequest request, final EntityDetails entityDetails) http1Config, connectionReuseStrategy, exchangeHandlerFactory, + exceptionCallback, context); outgoing = streamHandler; } else { @@ -365,6 +377,7 @@ void consumeHeader(final HttpRequest request, final EntityDetails entityDetails) http1Config, connectionReuseStrategy, exchangeHandlerFactory, + exceptionCallback, context); pipeline.add(streamHandler); } @@ -409,6 +422,11 @@ void execute(final RequestExecutionCommand executionCommand) throws HttpExceptio throw new HttpException("Illegal command: " + executionCommand.getClass()); } + @Override + boolean isRequestInitiated() { + return false; + } + @Override boolean isOutputReady() { return outgoing != null && outgoing.isOutputReady(); @@ -494,6 +512,10 @@ public void close() { channel.close(); } + public void close(final CloseMode closeMode) { + channel.close(closeMode); + } + @Override public void submit( final HttpResponse response, diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexerFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexerFactory.java index 7c078b6e7d..367110c21a 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexerFactory.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamDuplexerFactory.java @@ -30,6 +30,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.ContentLengthStrategy; import org.apache.hc.core5.http.HttpRequest; @@ -66,6 +67,7 @@ public final class ServerHttp1StreamDuplexerFactory { private final ContentLengthStrategy incomingContentStrategy; private final ContentLengthStrategy outgoingContentStrategy; private final Http1StreamListener streamListener; + private final Callback exceptionCallback; public ServerHttp1StreamDuplexerFactory( final HttpProcessor httpProcessor, @@ -77,7 +79,8 @@ public ServerHttp1StreamDuplexerFactory( final NHttpMessageWriterFactory responseWriterFactory, final ContentLengthStrategy incomingContentStrategy, final ContentLengthStrategy outgoingContentStrategy, - final Http1StreamListener streamListener) { + final Http1StreamListener streamListener, + final Callback exceptionCallback) { this.httpProcessor = Args.notNull(httpProcessor, "HTTP processor"); this.exchangeHandlerFactory = Args.notNull(exchangeHandlerFactory, "Exchange handler factory"); this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT; @@ -93,6 +96,7 @@ public ServerHttp1StreamDuplexerFactory( this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy : DefaultContentLengthStrategy.INSTANCE; this.streamListener = streamListener; + this.exceptionCallback = exceptionCallback; } public ServerHttp1StreamDuplexerFactory( @@ -103,10 +107,11 @@ public ServerHttp1StreamDuplexerFactory( final ConnectionReuseStrategy connectionReuseStrategy, final NHttpMessageParserFactory requestParserFactory, final NHttpMessageWriterFactory responseWriterFactory, - final Http1StreamListener streamListener) { + final Http1StreamListener streamListener, + final Callback exceptionCallback) { this(httpProcessor, exchangeHandlerFactory, http1Config, charCodingConfig, connectionReuseStrategy, requestParserFactory, responseWriterFactory, - null, null, streamListener); + null, null, streamListener, exceptionCallback); } public ServerHttp1StreamDuplexerFactory( @@ -114,8 +119,10 @@ public ServerHttp1StreamDuplexerFactory( final HandlerFactory exchangeHandlerFactory, final Http1Config http1Config, final CharCodingConfig charCodingConfig, - final Http1StreamListener streamListener) { - this(httpProcessor, exchangeHandlerFactory, http1Config, charCodingConfig, null, null ,null, streamListener); + final Http1StreamListener streamListener, + final Callback exceptionCallback) { + this(httpProcessor, exchangeHandlerFactory, http1Config, charCodingConfig, null, null ,null, + streamListener, exceptionCallback); } public ServerHttp1StreamDuplexer create(final String scheme, final ProtocolIOSession ioSession) { @@ -128,7 +135,8 @@ public ServerHttp1StreamDuplexer create(final String scheme, final ProtocolIOSes responseWriterFactory.create(), incomingContentStrategy, outgoingContentStrategy, - streamListener); + streamListener, + exceptionCallback); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamHandler.java index a03ce059ec..f0e238e7d6 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/nio/ServerHttp1StreamHandler.java @@ -30,7 +30,9 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; @@ -62,6 +64,7 @@ import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.apache.hc.core5.io.CloseMode; class ServerHttp1StreamHandler implements ResourceHolder { @@ -70,17 +73,18 @@ class ServerHttp1StreamHandler implements ResourceHolder { private final ResponseChannel responseChannel; private final HttpProcessor httpProcessor; private final Http1Config http1Config; - private final HandlerFactory exchangeHandlerFactory; private final ConnectionReuseStrategy connectionReuseStrategy; + private final HandlerFactory exchangeHandlerFactory; + private final Callback exceptionCallback; private final HttpCoreContext context; + private final AtomicReference requestState; + private final AtomicReference responseState; private final AtomicBoolean responseCommitted; private final AtomicBoolean done; private volatile boolean keepAlive; private volatile AsyncServerExchangeHandler exchangeHandler; private volatile HttpRequest receivedRequest; - private volatile MessageState requestState; - private volatile MessageState responseState; ServerHttp1StreamHandler( final Http1StreamChannel outputChannel, @@ -88,6 +92,7 @@ class ServerHttp1StreamHandler implements ResourceHolder { final Http1Config http1Config, final ConnectionReuseStrategy connectionReuseStrategy, final HandlerFactory exchangeHandlerFactory, + final Callback exceptionCallback, final HttpCoreContext context) { this.outputChannel = outputChannel; this.internalDataChannel = new DataStreamChannel() { @@ -99,11 +104,11 @@ public void requestOutput() { @Override public void endStream(final List trailers) throws IOException { + responseState.set( MessageState.COMPLETE); outputChannel.complete(trailers); - if (!keepAlive) { + if (requestState.get() == MessageState.COMPLETE && !keepAlive) { outputChannel.close(); } - responseState = MessageState.COMPLETE; } @Override @@ -137,6 +142,11 @@ public void pushPromise( commitPromise(); } + @Override + public void terminateExchange() { + terminate(); + } + @Override public String toString() { return super.toString() + " " + ServerHttp1StreamHandler.this; @@ -148,12 +158,13 @@ public String toString() { this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT; this.connectionReuseStrategy = connectionReuseStrategy; this.exchangeHandlerFactory = exchangeHandlerFactory; + this.exceptionCallback = exceptionCallback; this.context = context; + this.requestState = new AtomicReference<>(MessageState.HEADERS); + this.responseState = new AtomicReference<>(MessageState.IDLE); this.responseCommitted = new AtomicBoolean(); this.done = new AtomicBoolean(); this.keepAlive = true; - this.requestState = MessageState.HEADERS; - this.responseState = MessageState.IDLE; } private void commitResponse( @@ -178,21 +189,24 @@ private void commitResponse( httpProcessor.process(response, responseEntityDetails, context); final boolean endStream = responseEntityDetails == null || - (receivedRequest != null && Method.HEAD.isSame(receivedRequest.getMethod())); + receivedRequest != null && Method.HEAD.isSame(receivedRequest.getMethod()); if (!connectionReuseStrategy.keepAlive(receivedRequest, response, context)) { keepAlive = false; } - outputChannel.submit(response, endStream, endStream ? FlushMode.IMMEDIATE : FlushMode.BUFFER); if (endStream) { + responseState.set(MessageState.COMPLETE); + outputChannel.submit(response, true, FlushMode.IMMEDIATE); if (!keepAlive) { outputChannel.close(); } - responseState = MessageState.COMPLETE; } else { - responseState = MessageState.BODY; + outputChannel.submit(response, false, FlushMode.BUFFER); exchangeHandler.produce(internalDataChannel); + if (responseState.compareAndSet(MessageState.IDLE, MessageState.BODY)) { + outputChannel.requestOutput(); + } } } else { throw new HttpException("Response already committed"); @@ -214,12 +228,16 @@ private void commitPromise() throws HttpException { throw new HttpException("HTTP/1.1 does not support server push"); } + private void terminate() { + outputChannel.close(CloseMode.IMMEDIATE); + } + void activateChannel() throws IOException, HttpException { outputChannel.activate(); } boolean isResponseFinal() { - return responseState == MessageState.COMPLETE; + return responseState.get() == MessageState.COMPLETE; } boolean keepAlive() { @@ -227,15 +245,15 @@ boolean keepAlive() { } boolean isCompleted() { - return requestState == MessageState.COMPLETE && responseState == MessageState.COMPLETE; + return requestState.get() == MessageState.COMPLETE && responseState.get() == MessageState.COMPLETE; } void terminateExchange(final HttpException ex) throws HttpException, IOException { - if (done.get() || requestState != MessageState.HEADERS) { + if (done.get() || requestState.get() != MessageState.HEADERS) { throw new ProtocolException("Unexpected message head"); } receivedRequest = null; - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); final HttpResponse response = new BasicHttpResponse(ServerSupport.toStatusCode(ex)); response.addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE); final AsyncResponseProducer responseProducer = new BasicResponseProducer(response, ServerSupport.toErrorMessage(ex)); @@ -244,29 +262,28 @@ void terminateExchange(final HttpException ex) throws HttpException, IOException } void consumeHeader(final HttpRequest request, final EntityDetails requestEntityDetails) throws HttpException, IOException { - if (done.get() || requestState != MessageState.HEADERS) { + if (done.get() || requestState.get() != MessageState.HEADERS) { throw new ProtocolException("Unexpected message head"); } receivedRequest = request; - requestState = requestEntityDetails == null ? MessageState.COMPLETE : MessageState.BODY; - - final ProtocolVersion transportVersion = request.getVersion(); - if (transportVersion != null && transportVersion.greaterEquals(HttpVersion.HTTP_2)) { - throw new UnsupportedHttpVersionException(transportVersion); - } - context.setProtocolVersion(transportVersion != null ? transportVersion : http1Config.getVersion()); - context.setRequest(request); - + requestState.set(requestEntityDetails == null ? MessageState.COMPLETE : MessageState.BODY); try { + final ProtocolVersion transportVersion = request.getVersion(); + if (transportVersion != null && transportVersion.greaterEquals(HttpVersion.HTTP_2)) { + throw new UnsupportedHttpVersionException(transportVersion); + } + context.setProtocolVersion(transportVersion != null ? transportVersion : http1Config.getVersion()); + context.setRequest(request); + httpProcessor.process(request, requestEntityDetails, context); AsyncServerExchangeHandler handler; try { handler = exchangeHandlerFactory.create(request, context); } catch (final MisdirectedRequestException ex) { - handler = new ImmediateResponseExchangeHandler(HttpStatus.SC_MISDIRECTED_REQUEST, ex.getMessage()); + handler = new ImmediateResponseExchangeHandler(HttpStatus.SC_MISDIRECTED_REQUEST, ex.getMessage()); } catch (final HttpException ex) { - handler = new ImmediateResponseExchangeHandler(HttpStatus.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); + handler = new ImmediateResponseExchangeHandler(HttpStatus.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); } if (handler == null) { handler = new ImmediateResponseExchangeHandler(HttpStatus.SC_NOT_FOUND, "Cannot handle request"); @@ -289,7 +306,7 @@ void consumeHeader(final HttpRequest request, final EntityDetails requestEntityD } boolean isOutputReady() { - switch (responseState) { + switch (responseState.get()) { case BODY: return exchangeHandler.available() > 0; default: @@ -298,7 +315,7 @@ boolean isOutputReady() { } void produceOutput() throws IOException { - switch (responseState) { + switch (responseState.get()) { case BODY: exchangeHandler.produce(internalDataChannel); break; @@ -306,10 +323,10 @@ void produceOutput() throws IOException { } void consumeData(final ByteBuffer src) throws HttpException, IOException { - if (done.get() || requestState != MessageState.BODY) { + if (done.get() || requestState.get() != MessageState.BODY) { throw new ProtocolException("Unexpected message data"); } - if (responseState == MessageState.ACK) { + if (responseState.get() == MessageState.ACK) { outputChannel.requestOutput(); } exchangeHandler.consume(src); @@ -320,31 +337,38 @@ void updateCapacity(final CapacityChannel capacityChannel) throws IOException { } void dataEnd(final List trailers) throws HttpException, IOException { - if (done.get() || requestState != MessageState.BODY) { + if (done.get() || requestState.get() != MessageState.BODY) { throw new ProtocolException("Unexpected message data"); } - requestState = MessageState.COMPLETE; + requestState.set(MessageState.COMPLETE); + if (responseState.get() == MessageState.COMPLETE && !keepAlive) { + outputChannel.close(); + } exchangeHandler.streamEnd(trailers); } void failed(final Exception cause) { - if (!done.get()) { + if (!done.get() && exchangeHandler != null) { exchangeHandler.failed(cause); + } else if (exceptionCallback != null) { + exceptionCallback.execute(cause); } } @Override public void releaseResources() { if (done.compareAndSet(false, true)) { - requestState = MessageState.COMPLETE; - responseState = MessageState.COMPLETE; - exchangeHandler.releaseResources(); + requestState.set(MessageState.COMPLETE); + responseState.set(MessageState.COMPLETE); + if (exchangeHandler != null) { + exchangeHandler.releaseResources(); + } } } void appendState(final StringBuilder buf) { - buf.append("requestState=").append(requestState) - .append(", responseState=").append(responseState) + buf.append("requestState=").append(requestState.get()) + .append(", responseState=").append(responseState.get()) .append(", responseCommitted=").append(responseCommitted) .append(", keepAlive=").append(keepAlive) .append(", done=").append(done); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/PathPatternMatcher.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/PathPatternMatcher.java index 5517f4e888..04af58e21f 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/PathPatternMatcher.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/PathPatternMatcher.java @@ -52,14 +52,14 @@ public boolean match(final String pattern, final String path) { if (pattern.equals("*") || pattern.equals(path)) { return true; } - return (pattern.endsWith("*") && path.startsWith(pattern.substring(0, pattern.length() - 1))) - || (pattern.startsWith("*") && path.endsWith(pattern.substring(1))); + return pattern.endsWith("*") && path.startsWith(pattern.substring(0, pattern.length() - 1)) + || pattern.startsWith("*") && path.endsWith(pattern.substring(1)); } public boolean isBetter(final String pattern, final String bestMatch) { return bestMatch == null - || (bestMatch.length() < pattern.length()) - || (bestMatch.length() == pattern.length() && pattern.endsWith("*")); + || bestMatch.length() < pattern.length() + || bestMatch.length() == pattern.length() && pattern.endsWith("*"); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/RequestRouter.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/RequestRouter.java index b0d27c89da..d802bbce8d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/RequestRouter.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/RequestRouter.java @@ -81,13 +81,13 @@ public String toString() { } - static class SingleAuthorityResolver implements Function { + static final class SingleAuthorityResolver implements Function { private final URIAuthority singleAuthority; private final T router; SingleAuthorityResolver(final URIAuthority singleAuthority, final T router) { - this.singleAuthority = singleAuthority; + this.singleAuthority = Args.notNull(singleAuthority, "singleAuthority"); this.router = router; } @@ -103,13 +103,8 @@ public String toString() { } - static class NoAuthorityResolver implements Function { - - @Override - public T apply(final URIAuthority authority) { - return null; - } - + static final Function noAuthorityResolver() { + return u -> null; } @Internal @@ -136,7 +131,7 @@ public static RequestRouter create(final URIAuthority primaryAuthority, })))); final Function> authorityFunction; if (authorityMap.isEmpty()) { - authorityFunction = new NoAuthorityResolver<>(); + authorityFunction = noAuthorityResolver(); } else if (authorityMap.size() == 1) { final Map.Entry> entry = authorityMap.entrySet().iterator().next(); authorityFunction = new SingleAuthorityResolver<>(entry.getKey(), entry.getValue()); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/UriPathRouter.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/UriPathRouter.java index 24a92d60e8..8528953405 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/UriPathRouter.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/routing/UriPathRouter.java @@ -60,15 +60,15 @@ public String toString() { } static UriPathRouter bestMatch(final List> routes) { - return new UriPathRouter<>(e -> e, new BestMatcher<>(), routes); + return new UriPathRouter<>(Function.identity(), BestMatcher.getInstance(), routes); } static UriPathRouter ordered(final List> routes) { - return new UriPathRouter<>(e -> e, new OrderedMatcher<>(), routes); + return new UriPathRouter<>(Function.identity(), OrderedMatcher.getInstance(), routes); } static UriPathRouter regEx(final List> routes) { - return new UriPathRouter<>(Pattern::compile, new RegexMatcher<>(), routes); + return new UriPathRouter<>(Pattern::compile, RegexMatcher.getInstance(), routes); } private static final PathPatternMatcher PATH_PATTERN_MATCHER = PathPatternMatcher.INSTANCE; @@ -83,9 +83,24 @@ static UriPathRouter regEx(final List> routes) { *

  • {@code *}
  • *
  • {@code *}
  • * + *

    + * This class has no instance state. + *

    */ final static class BestMatcher implements BiFunction>, T> { + @SuppressWarnings("rawtypes") // raw by design + private static final BestMatcher INSTANCE = new BestMatcher(); + + @SuppressWarnings({ "cast", "unchecked" }) // cast to call site + static BestMatcher getInstance() { + return (BestMatcher) INSTANCE; + } + + private BestMatcher() { + // singleton instance only + } + @Override public T apply(final String path, final List> routes) { PathRoute bestMatch = null; @@ -115,9 +130,24 @@ public T apply(final String path, final List> routes) { *
  • {@code *}
  • *
  • {@code *}
  • * + *

    + * This class has no instance state. + *

    */ final static class OrderedMatcher implements BiFunction>, T> { + @SuppressWarnings("rawtypes") // raw by design + private static final OrderedMatcher INSTANCE = new OrderedMatcher(); + + @SuppressWarnings({ "cast", "unchecked" }) // cast to call site + static OrderedMatcher getInstance() { + return (OrderedMatcher) INSTANCE; + } + + private OrderedMatcher() { + // singleton instance only + } + @Override public T apply(final String path, final List> routes) { for (final PathRoute route : routes) { @@ -135,9 +165,24 @@ public T apply(final String path, final List> routes) { /** * Finds a match for the given path from a collection of regular expressions. + *

    + * This class has no instance state. + *

    */ final static class RegexMatcher implements BiFunction>, T> { + @SuppressWarnings("rawtypes") // raw by design + private static final RegexMatcher INSTANCE = new RegexMatcher(); + + @SuppressWarnings({ "cast", "unchecked" }) // cast to call site + static RegexMatcher getInstance() { + return (RegexMatcher) INSTANCE; + } + + private RegexMatcher() { + // singleton instance only + } + @Override public T apply(final String path, final List> routes) { for (final PathRoute route : routes) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/EofSensorInputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/EofSensorInputStream.java index 2da06ad069..d22184840f 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/EofSensorInputStream.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/EofSensorInputStream.java @@ -135,7 +135,7 @@ public int read(final byte[] b, final int off, final int len) throws IOException if (isReadAllowed()) { try { - readLen = wrappedStream.read(b, off, len); + readLen = wrappedStream.read(b, off, len); checkEOF(readLen); } catch (final IOException ex) { checkAbort(); @@ -195,7 +195,7 @@ public void close() throws IOException { private void checkEOF(final int eof) throws IOException { final InputStream toCheckStream = wrappedStream; - if ((toCheckStream != null) && (eof < 0)) { + if (toCheckStream != null && eof < 0) { try { boolean scws = true; // should close wrapped stream? if (eofWatcher != null) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BufferedHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BufferedHttpEntity.java index e7c061bf4c..f8b2c96784 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BufferedHttpEntity.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BufferedHttpEntity.java @@ -91,7 +91,7 @@ public InputStream getContent() throws IOException { */ @Override public boolean isChunked() { - return (buffer == null) && super.isChunked(); + return buffer == null && super.isChunked(); } /** @@ -119,7 +119,7 @@ public void writeTo(final OutputStream outStream) throws IOException { // non-javadoc, see interface HttpEntity @Override public boolean isStreaming() { - return (buffer == null) && super.isStreaming(); + return buffer == null && super.isStreaming(); } } // class BufferedHttpEntity diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/ByteArrayEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/ByteArrayEntity.java index 9f5976420b..d7fa821b4d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/ByteArrayEntity.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/ByteArrayEntity.java @@ -198,7 +198,7 @@ public ByteArrayEntity(final byte[] buf, final ContentType contentType) { * @param chunked Whether this entity should be chunked. */ public ByteArrayEntity( - final byte[] buf, final int off, final int len, final ContentType contentType, final boolean chunked) { + final byte[] buf, final int off, final int len, final ContentType contentType, final boolean chunked) { this(buf, off, len, contentType, null, chunked); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityTemplate.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityTemplate.java index 448f3d8379..34fe98f276 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityTemplate.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityTemplate.java @@ -77,6 +77,23 @@ public EntityTemplate( this.callback = Args.notNull(callback, "I/O callback"); } + /** + * @since 5.4 + */ + public EntityTemplate( + final long contentLength, + final ContentType contentType, + final IOCallback callback) { + this(contentLength, contentType, null, callback); + } + + /** + * @since 5.4 + */ + public EntityTemplate(final ContentType contentType, final IOCallback callback) { + this(-1, contentType, null, callback); + } + @Override public long getContentLength() { return contentLength; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java index 508d89b7b3..79ddb978c1 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java @@ -84,7 +84,7 @@ public static HttpEntity create(final Serializable serializable, final ContentTy } public static HttpEntity createUrlEncoded( - final Iterable parameters, final Charset charset) { + final Iterable parameters, final Charset charset) { final ContentType contentType = charset != null ? ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset) : ContentType.APPLICATION_FORM_URLENCODED; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java index b4b18058a2..9138edbafa 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java @@ -27,12 +27,6 @@ package org.apache.hc.core5.http.io.entity; -import org.apache.hc.core5.annotation.Contract; -import org.apache.hc.core5.annotation.ThreadingBehavior; -import org.apache.hc.core5.function.Supplier; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpEntity; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -40,6 +34,12 @@ import java.util.List; import java.util.Set; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; + /** * An empty entity with no content-type. This type may be used for convenience * in place of an empty {@link ByteArrayEntity}. @@ -51,7 +51,8 @@ public final class NullEntity implements HttpEntity { public static final NullEntity INSTANCE = new NullEntity(); - private NullEntity() {} + private NullEntity() { + } @Override public boolean isRepeatable() { @@ -64,7 +65,8 @@ public InputStream getContent() throws IOException, UnsupportedOperationExceptio } @Override - public void writeTo(final OutputStream outStream) throws IOException {} + public void writeTo(final OutputStream outStream) throws IOException { + } @Override public boolean isStreaming() { @@ -77,7 +79,8 @@ public Supplier> getTrailers() { } @Override - public void close() throws IOException {} + public void close() throws IOException { + } @Override public long getContentLength() { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java index 5dd0c1f2fa..dc4a6c8e1d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilder.java @@ -33,11 +33,13 @@ import java.util.Arrays; import java.util.List; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.ProtocolVersion; @@ -120,6 +122,44 @@ public static ClassicRequestBuilder head(final String uri) { return new ClassicRequestBuilder(Method.HEAD, uri); } + /** + * Initializes a new {@link ClassicRequestBuilder} instance for the {@code QUERY} method. + * + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static ClassicRequestBuilder query() { + return new ClassicRequestBuilder(Method.QUERY); + } + + /** + * Initializes a new {@link ClassicRequestBuilder} instance for the {@code QUERY} method. + * + * @param uri the request URI. + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static ClassicRequestBuilder query(final URI uri) { + return new ClassicRequestBuilder(Method.QUERY, uri); + } + + /** + * Initializes a new {@link ClassicRequestBuilder} instance for the {@code QUERY} method. + * + * @param uri the request URI. + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static ClassicRequestBuilder query(final String uri) { + return new ClassicRequestBuilder(Method.QUERY, uri); + } + public static ClassicRequestBuilder patch() { return new ClassicRequestBuilder(Method.PATCH); } @@ -202,6 +242,16 @@ public static ClassicRequestBuilder copy(final ClassicHttpRequest request) { return builder; } + /** + * @since 5.4 + */ + public static ClassicRequestBuilder copy(final HttpRequest request) { + Args.notNull(request, "HTTP request"); + final ClassicRequestBuilder builder = new ClassicRequestBuilder(request.getMethod()); + builder.digest(request); + return builder; + } + protected void digest(final ClassicHttpRequest request) { super.digest(request); setEntity(request.getEntity()); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicResponseBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicResponseBuilder.java index 385b59f064..f33122e4a7 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicResponseBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/support/ClassicResponseBuilder.java @@ -33,6 +33,7 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.ProtocolVersion; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.StringEntity; @@ -68,6 +69,16 @@ public static ClassicResponseBuilder copy(final ClassicHttpResponse response) { return builder; } + /** + * @since 5.4 + */ + public static ClassicResponseBuilder copy(final HttpResponse response) { + Args.notNull(response, "HTTP response"); + final ClassicResponseBuilder builder = new ClassicResponseBuilder(response.getCode()); + builder.digest(response); + return builder; + } + protected void digest(final ClassicHttpResponse response) { super.digest(response); setEntity(response.getEntity()); @@ -92,7 +103,7 @@ public ClassicResponseBuilder addHeader(final Header header) { } @Override - public ClassicResponseBuilder addHeader(final String name, final String value) { + public ClassicResponseBuilder addHeader(final String name, final String value) { super.addHeader(name, value); return this; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderIterator.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderIterator.java index fdaba87209..524fd0bd90 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderIterator.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderIterator.java @@ -89,9 +89,9 @@ private int findNext(final int pos) { return -1; } - final int to = this.allHeaders.length-1; + final int to = this.allHeaders.length - 1; boolean found = false; - while (!found && (from < to)) { + while (!found && from < to) { from++; found = filterHeader(from); } @@ -107,7 +107,7 @@ private int findNext(final int pos) { * iteration, {@code false} to skip */ private boolean filterHeader(final int index) { - return (this.headerName == null) || + return this.headerName == null || this.headerName.equalsIgnoreCase(this.allHeaders[index].getName()); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueFormatter.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueFormatter.java index 383d286972..b3dcbc5828 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueFormatter.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueFormatter.java @@ -119,7 +119,7 @@ void formatValue(final CharArrayBuffer buffer, final String value, final boolean boolean quoteFlag = quote; if (!quoteFlag) { - for (int i = 0; (i < value.length()) && !quoteFlag; i++) { + for (int i = 0; i < value.length() && !quoteFlag; i++) { quoteFlag = isSeparator(value.charAt(i)); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueParser.java index d21b061e59..a395dbc546 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueParser.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicHeaderValueParser.java @@ -47,8 +47,8 @@ public class BasicHeaderValueParser implements HeaderValueParser { public final static BasicHeaderValueParser INSTANCE = new BasicHeaderValueParser(); - private final static char PARAM_DELIMITER = ';'; - private final static char ELEM_DELIMITER = ','; + private final static char PARAM_DELIMITER = ';'; + private final static char ELEM_DELIMITER = ','; private static final Tokenizer.Delimiter TOKEN_DELIMITER = Tokenizer.delimiters('=', PARAM_DELIMITER, ELEM_DELIMITER); private static final Tokenizer.Delimiter VALUE_DELIMITER = Tokenizer.delimiters(PARAM_DELIMITER, ELEM_DELIMITER); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java index 003fd218c1..b7b4282534 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java @@ -67,7 +67,7 @@ public class BasicLineParser implements LineParser { * is not relevant, only the protocol name. */ public BasicLineParser(final ProtocolVersion proto) { - this.protocol = proto != null? proto : HttpVersion.HTTP_1_1; + this.protocol = proto != null ? proto : HttpVersion.HTTP_1_1; this.tokenizer = Tokenizer.INSTANCE; } @@ -82,7 +82,7 @@ ProtocolVersion parseProtocolVersion( final CharArrayBuffer buffer, final ParserCursor cursor) throws ParseException { final String protoname = this.protocol.getProtocol(); - final int protolength = protoname.length(); + final int protolength = protoname.length(); this.tokenizer.skipWhiteSpace(buffer, cursor); @@ -96,7 +96,7 @@ ProtocolVersion parseProtocolVersion( // check the protocol name and slash boolean ok = true; - for (int i = 0; ok && (i < protolength); i++) { + for (int i = 0; ok && i < protolength; i++) { ok = buffer.charAt(pos + i) == protoname.charAt(i); } if (ok) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicListHeaderIterator.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicListHeaderIterator.java index 8f11f7cb82..5cc1b7bb91 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicListHeaderIterator.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicListHeaderIterator.java @@ -96,9 +96,9 @@ protected int findNext(final int pos) { return -1; } - final int to = this.allHeaders.size()-1; + final int to = this.allHeaders.size() - 1; boolean found = false; - while (!found && (from < to)) { + while (!found && from < to) { from++; found = filterHeader(from); } @@ -143,7 +143,7 @@ public Header next() throws NoSuchElementException { throw new NoSuchElementException("Iteration already finished."); } - this.lastIndex = current; + this.lastIndex = current; this.currentIndex = findNext(current); return this.allHeaders.get(current); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java index b0c6a446bc..43a360dde8 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/MessageSupport.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiConsumer; import java.util.function.Consumer; import org.apache.hc.core5.http.EntityDetails; @@ -168,48 +169,107 @@ public static Header format(final String name, final String... tokens) { } /** - * @since 5.3 + * @since 5.4 */ - public static void parseTokens(final CharSequence src, final ParserCursor cursor, final Consumer consumer) { + public static void parseHeader(final Header header, final BiConsumer consumer) { + Args.notNull(header, "Header"); + if (header instanceof FormattedHeader) { + final CharArrayBuffer buf = ((FormattedHeader) header).getBuffer(); + final ParserCursor cursor = new ParserCursor(0, buf.length()); + cursor.updatePos(((FormattedHeader) header).getValuePos()); + consumer.accept(buf, cursor); + } else { + final String value = header.getValue(); + final ParserCursor cursor = new ParserCursor(0, value.length()); + consumer.accept(value, cursor); + } + } + + /** + * @since 5.4 + */ + public static void parseHeaders(final MessageHeaders headers, final String name, final BiConsumer consumer) { + Args.notNull(headers, "Message headers"); + Args.notBlank(name, "Header name"); + final Iterator
    it = headers.headerIterator(name); + while (it.hasNext()) { + parseHeader(it.next(), consumer); + } + } + + /** + * @since 5.4 + */ + public static void parseElementList(final CharSequence src, + final ParserCursor cursor, + final BiConsumer consumer) { Args.notNull(src, "Source"); Args.notNull(cursor, "Cursor"); Args.notNull(consumer, "Consumer"); while (!cursor.atEnd()) { - final int pos = cursor.getPos(); - if (src.charAt(pos) == ',') { - cursor.updatePos(pos + 1); + consumer.accept(src, cursor); + if (!cursor.atEnd()) { + final char ch = src.charAt(cursor.getPos()); + if (ch == ',') { + cursor.updatePos(cursor.getPos() + 1); + } } - final String token = Tokenizer.INSTANCE.parseToken(src, cursor, COMMA); - consumer.accept(token); } } + /** + * @since 5.4 + */ + public static void parseTokens(final CharSequence src, + final ParserCursor cursor, + final Tokenizer.Delimiter delimiterPredicate, + final Consumer consumer) { + parseElementList(src, cursor, (sequence, c) -> { + final String token = Tokenizer.INSTANCE.parseToken(src, c, delimiterPredicate); + consumer.accept(token); + }); + } + + /** + * @since 5.3 + */ + public static void parseTokens(final CharSequence src, final ParserCursor cursor, final Consumer consumer) { + parseTokens(src, cursor, COMMA, consumer); + } + + /** + * @since 5.4 + */ + public static void parseTokens(final Header header, + final Tokenizer.Delimiter delimiterPredicate, + final Consumer consumer) { + parseHeader(header, (sequence, cursor) -> + parseTokens(sequence, cursor, delimiterPredicate, consumer)); + } + /** * @since 5.3 */ public static void parseTokens(final Header header, final Consumer consumer) { - Args.notNull(header, "Header"); - if (header instanceof FormattedHeader) { - final CharArrayBuffer buf = ((FormattedHeader) header).getBuffer(); - final ParserCursor cursor = new ParserCursor(0, buf.length()); - cursor.updatePos(((FormattedHeader) header).getValuePos()); - parseTokens(buf, cursor, consumer); - } else { - final String value = header.getValue(); - final ParserCursor cursor = new ParserCursor(0, value.length()); - parseTokens(value, cursor, consumer); - } + parseTokens(header, COMMA, consumer); + } + + /** + * @since 5.4 + */ + public static void parseTokens(final MessageHeaders headers, + final String headerName, + final Tokenizer.Delimiter delimiterPredicate, + final Consumer consumer) { + parseHeaders(headers, headerName, (sequence, cursor) -> + parseTokens(sequence, cursor, delimiterPredicate, consumer)); } /** * @since 5.3 */ public static void parseTokens(final MessageHeaders headers, final String headerName, final Consumer consumer) { - Args.notNull(headers, "Headers"); - final Iterator
    it = headers.headerIterator(headerName); - while (it.hasNext()) { - parseTokens(it.next(), consumer); - } + parseTokens(headers, headerName, COMMA, consumer); } public static Set parseTokens(final CharSequence src, final ParserCursor cursor) { @@ -291,19 +351,10 @@ public static Header header(final String name, final HeaderElement... elements) * @since 5.3 */ public static void parseElements(final CharSequence buffer, final ParserCursor cursor, final Consumer consumer) { - Args.notNull(buffer, "Char sequence"); - Args.notNull(cursor, "Parser cursor"); - Args.notNull(consumer, "Consumer"); - while (!cursor.atEnd()) { + parseElementList(buffer, cursor, (sequence, c) -> { final HeaderElement element = BasicHeaderValueParser.INSTANCE.parseHeaderElement(buffer, cursor); consumer.accept(element); - if (!cursor.atEnd()) { - final char ch = buffer.charAt(cursor.getPos()); - if (ch == ',') { - cursor.updatePos(cursor.getPos() + 1); - } - } - } + }); } /** @@ -311,16 +362,8 @@ public static void parseElements(final CharSequence buffer, final ParserCursor c */ public static void parseElements(final Header header, final Consumer consumer) { Args.notNull(header, "Header"); - if (header instanceof FormattedHeader) { - final CharArrayBuffer buf = ((FormattedHeader) header).getBuffer(); - final ParserCursor cursor = new ParserCursor(0, buf.length()); - cursor.updatePos(((FormattedHeader) header).getValuePos()); - parseElements(buf, cursor, consumer); - } else { - final String value = header.getValue(); - final ParserCursor cursor = new ParserCursor(0, value.length()); - parseElements(value, cursor, consumer); - } + parseHeader(header, (sequence, cursor) -> + parseElements(sequence, cursor, consumer)); } /** @@ -328,10 +371,8 @@ public static void parseElements(final Header header, final Consumer consumer) { Args.notNull(headers, "Headers"); - final Iterator
    it = headers.headerIterator(headerName); - while (it.hasNext()) { - parseElements(it.next(), consumer); - } + parseHeaders(headers, headerName, (sequence, cursor) -> + parseElements(sequence, cursor, consumer)); } /** @@ -438,6 +479,16 @@ public static void addTrailerHeader(final HttpMessage message, final EntityDetai } } + /** + * @since 5.4 + */ + public static boolean canResponseHaveBody(final HttpResponse response) { + final int status = response.getCode(); + return status >= HttpStatus.SC_SUCCESS + && status != HttpStatus.SC_NO_CONTENT + && status != HttpStatus.SC_NOT_MODIFIED; + } + /** * @since 5.0 */ @@ -449,9 +500,7 @@ public static boolean canResponseHaveBody(final String method, final HttpRespons if (Method.CONNECT.isSame(method) && status == HttpStatus.SC_OK) { return false; } - return status >= HttpStatus.SC_SUCCESS - && status != HttpStatus.SC_NO_CONTENT - && status != HttpStatus.SC_NOT_MODIFIED; + return canResponseHaveBody(response); } private final static Set HOP_BY_HOP; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/AsyncClientEndpoint.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/AsyncClientEndpoint.java index 749bf32c24..94def76b90 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/AsyncClientEndpoint.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/AsyncClientEndpoint.java @@ -32,8 +32,8 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.concurrent.BasicFuture; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.http.nio.support.BasicClientExchangeHandler; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; @@ -101,14 +101,7 @@ public final Future execute( final FutureCallback callback) { final BasicFuture future = new BasicFuture<>(callback); execute(new BasicClientExchangeHandler<>(requestProducer, responseConsumer, - new FutureContribution(future) { - - @Override - public void completed(final T result) { - future.completed(result); - } - - }), + new CompletingFutureContribution(future)), pushHandlerFactory, context != null ? context : HttpCoreContext.create()); return future; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/ResponseChannel.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/ResponseChannel.java index 155af155ba..8a477634c2 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/ResponseChannel.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/ResponseChannel.java @@ -30,6 +30,7 @@ import java.io.IOException; import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.HttpException; @@ -82,4 +83,12 @@ public interface ResponseChannel { */ void pushPromise(HttpRequest promise, AsyncPushProducer responseProducer, HttpContext context) throws HttpException, IOException; + /** + * Terminates message exchange due to an internal error generating + * a response or its content. + */ + @Internal + default void terminateExchange() { + } + } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java index a7f156f399..b5a20243f1 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java @@ -81,7 +81,7 @@ public static AsyncEntityProducer create(final File content, final ContentType c } public static AsyncEntityProducer createUrlEncoded( - final Iterable parameters, final Charset charset) { + final Iterable parameters, final Charset charset) { final ContentType contentType = charset != null ? ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset) : ContentType.APPLICATION_FORM_URLENCODED; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityProducer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityProducer.java index 86502f5b5c..d1f89e842c 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityProducer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityProducer.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.http.ContentType; @@ -53,6 +54,7 @@ public class BasicAsyncEntityProducer implements AsyncEntityProducer { private final int length; private final ContentType contentType; private final boolean chunked; + private final AtomicBoolean endOfStream; private final AtomicReference exception; public BasicAsyncEntityProducer(final byte[] content, final ContentType contentType, final boolean chunked) { @@ -61,6 +63,7 @@ public BasicAsyncEntityProducer(final byte[] content, final ContentType contentT this.length = this.bytebuf.remaining(); this.contentType = contentType; this.chunked = chunked; + this.endOfStream = new AtomicBoolean(); this.exception = new AtomicReference<>(); } @@ -79,6 +82,7 @@ public BasicAsyncEntityProducer(final CharSequence content, final ContentType co this.bytebuf = charset.encode(CharBuffer.wrap(content)); this.length = this.bytebuf.remaining(); this.chunked = chunked; + this.endOfStream = new AtomicBoolean(); this.exception = new AtomicReference<>(); } @@ -130,7 +134,7 @@ public final void produce(final DataStreamChannel channel) throws IOException { if (bytebuf.hasRemaining()) { channel.write(bytebuf); } - if (!bytebuf.hasRemaining()) { + if (!bytebuf.hasRemaining() && endOfStream.compareAndSet(false, true)) { channel.endStream(); } } @@ -150,6 +154,7 @@ public final Exception getException() { public void releaseResources() { bytebuf.clear(); bytebuf.limit(length); + endOfStream.set(false); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractAsyncPushHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractAsyncPushHandler.java index f46dd201f6..1415c13d29 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractAsyncPushHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractAsyncPushHandler.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.EntityDetails; @@ -52,9 +53,11 @@ public abstract class AbstractAsyncPushHandler implements AsyncPushConsumer { private final AsyncResponseConsumer responseConsumer; + private final AtomicReference promiseRef; public AbstractAsyncPushHandler(final AsyncResponseConsumer responseConsumer) { this.responseConsumer = Args.notNull(responseConsumer, "Response consumer"); + this.promiseRef = new AtomicReference<>(); } /** @@ -83,6 +86,7 @@ public final void consumePromise( final HttpResponse response, final EntityDetails entityDetails, final HttpContext httpContext) throws HttpException, IOException { + promiseRef.compareAndSet(null, promise); responseConsumer.consumeResponse(response, entityDetails, httpContext, new FutureCallback() { @Override @@ -96,7 +100,6 @@ public void completed(final T result) { @Override public void failed(final Exception cause) { - handleError(promise, cause); releaseResources(); } @@ -126,6 +129,7 @@ public final void streamEnd(final List trailers) throws HttpEx @Override public final void failed(final Exception cause) { responseConsumer.failed(cause); + handleError(promiseRef.get(), cause); releaseResources(); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java index da217b0528..d33cfc6dbd 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java @@ -148,21 +148,23 @@ public void completed(final T result) { .build(), context); } catch (final HttpException | IOException ex2) { - failed(ex2); + failedInternal(ex2); } } catch (final IOException ex) { - failed(ex); + failedInternal(ex); + } finally { + releaseRequestConsumer(); } } @Override public void failed(final Exception ex) { - AbstractServerExchangeHandler.this.failed(ex); + failedInternal(ex); } @Override public void cancelled() { - releaseResources(); + releaseResourcesInternal(); } }); @@ -205,30 +207,54 @@ public final void produce(final DataStreamChannel channel) throws IOException { @Override public final void failed(final Exception cause) { + failedInternal(cause); + } + + void failedInternal(final Exception cause) { try { final AsyncRequestConsumer requestConsumer = requestConsumerRef.get(); if (requestConsumer != null) { requestConsumer.failed(cause); } + } finally { + releaseRequestConsumer(); + } + try { final AsyncResponseProducer dataProducer = responseProducerRef.get(); if (dataProducer != null) { dataProducer.failed(cause); } } finally { - releaseResources(); + releaseResponseProducer(); } } - @Override - public final void releaseResources() { + private void releaseRequestConsumer() { final AsyncRequestConsumer requestConsumer = requestConsumerRef.getAndSet(null); if (requestConsumer != null) { requestConsumer.releaseResources(); } + } + + private void releaseResponseProducer() { final AsyncResponseProducer dataProducer = responseProducerRef.getAndSet(null); if (dataProducer != null) { dataProducer.releaseResources(); } } + private void releaseResourcesInternal() { + releaseResponseProducer(); + releaseRequestConsumer(); + } + + @Override + public final void releaseResources() { + // Note even though the message exchange has been fully + // completed on the transport level, the request + // consumer may still be busy post-processing + // the request message. + releaseResponseProducer(); + } + } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java index 1c2b99b34a..7b16f97747 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java @@ -33,6 +33,7 @@ import java.util.Arrays; import java.util.List; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; @@ -120,6 +121,44 @@ public static AsyncRequestBuilder head(final String uri) { return new AsyncRequestBuilder(Method.HEAD, uri); } + /** + * Initializes a new {@link AsyncRequestBuilder} instance for the {@code QUERY} method. + * + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static AsyncRequestBuilder query() { + return new AsyncRequestBuilder(Method.QUERY); + } + + /** + * Initializes a new {@link AsyncRequestBuilder} instance for the {@code QUERY} method. + * + * @param uri the request URI. + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static AsyncRequestBuilder query(final URI uri) { + return new AsyncRequestBuilder(Method.QUERY, uri); + } + + /** + * Initializes a new {@link AsyncRequestBuilder} instance for the {@code QUERY} method. + * + * @param uri the request URI. + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static AsyncRequestBuilder query(final String uri) { + return new AsyncRequestBuilder(Method.QUERY, uri); + } + public static AsyncRequestBuilder patch() { return new AsyncRequestBuilder(Method.PATCH); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncResponseBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncResponseBuilder.java index 7c44c4301b..e055927a12 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncResponseBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncResponseBuilder.java @@ -44,7 +44,7 @@ * * @since 5.0 */ -public class AsyncResponseBuilder extends AbstractResponseBuilder { +public class AsyncResponseBuilder extends AbstractResponseBuilder { private AsyncEntityProducer entityProducer; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java index 467b9d637b..c74b7d319d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java @@ -59,8 +59,9 @@ public final class BasicClientExchangeHandler implements AsyncClientExchangeH private final AsyncRequestProducer requestProducer; private final AsyncResponseConsumer responseConsumer; private final AtomicBoolean completed; - private final FutureCallback resultCallback; private final AtomicBoolean outputTerminated; + private final AtomicBoolean inputTerminated; + private final FutureCallback resultCallback; public BasicClientExchangeHandler( final AsyncRequestProducer requestProducer, @@ -71,6 +72,7 @@ public BasicClientExchangeHandler( this.completed = new AtomicBoolean(); this.resultCallback = resultCallback; this.outputTerminated = new AtomicBoolean(); + this.inputTerminated = new AtomicBoolean(); } @Override @@ -100,48 +102,23 @@ public void consumeInformation(final HttpResponse response, final HttpContext ht @Override public void consumeResponse(final HttpResponse response, final EntityDetails entityDetails, final HttpContext httpContext) throws HttpException, IOException { if (response.getCode() >= HttpStatus.SC_CLIENT_ERROR) { - outputTerminated.set(true); - requestProducer.releaseResources(); + releaseRequestProducer(); } responseConsumer.consumeResponse(response, entityDetails, httpContext, new FutureCallback() { @Override public void completed(final T result) { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.completed(result); - } - } finally { - internalReleaseResources(); - } - } + completedInternal(result); } @Override public void failed(final Exception ex) { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.failed(ex); - } - } finally { - internalReleaseResources(); - } - } + failedInternal(ex); } @Override public void cancelled() { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.cancelled(); - } - } finally { - internalReleaseResources(); - } - } + cancelledInternal(); } }); @@ -149,15 +126,7 @@ public void cancelled() { @Override public void cancel() { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.cancelled(); - } - } finally { - internalReleaseResources(); - } - } + cancelledInternal(); } @Override @@ -178,28 +147,79 @@ public void streamEnd(final List trailers) throws HttpExceptio @Override public void failed(final Exception cause) { try { - requestProducer.failed(cause); - responseConsumer.failed(cause); + if (inputTerminated.compareAndSet(false, true)) { + responseConsumer.failed(cause); + responseConsumer.releaseResources(); + } + if (outputTerminated.compareAndSet(false, true)) { + requestProducer.failed(cause); + requestProducer.releaseResources(); + } } finally { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.failed(cause); - } - } finally { - internalReleaseResources(); + failedInternal(cause); + } + } + + private void completedInternal(final T result) { + if (completed.compareAndSet(false, true)) { + try { + if (resultCallback != null) { + resultCallback.completed(result); } + } finally { + releaseResourcesInternal(); } } } - private void internalReleaseResources() { - requestProducer.releaseResources(); - responseConsumer.releaseResources(); + private void failedInternal(final Exception ex) { + if (completed.compareAndSet(false, true)) { + try { + if (resultCallback != null) { + resultCallback.failed(ex); + } + } finally { + releaseResourcesInternal(); + } + } + } + + private void cancelledInternal() { + if (completed.compareAndSet(false, true)) { + try { + if (resultCallback != null) { + resultCallback.cancelled(); + } + } finally { + releaseResourcesInternal(); + } + } + } + + private void releaseResponseConsumer() { + if (inputTerminated.compareAndSet(false, true)) { + responseConsumer.releaseResources(); + } + } + + private void releaseRequestProducer() { + if (outputTerminated.compareAndSet(false, true)) { + requestProducer.releaseResources(); + } + } + + private void releaseResourcesInternal() { + releaseRequestProducer(); + releaseResponseConsumer(); } @Override public void releaseResources() { + // Note even though the message exchange has been fully + // completed on the transport level, the response + // consumer may still be busy consuming and digesting + // the response message + releaseRequestProducer(); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityConsumer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityConsumer.java index b8122dbd97..2952da1adf 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityConsumer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityConsumer.java @@ -52,7 +52,10 @@ * @param entity representation. * * @since 5.0 + * + * @deprecated Use {@link ClassicToAsyncResponseConsumer}. */ +@Deprecated public abstract class AbstractClassicEntityConsumer implements AsyncEntityConsumer { private enum State { IDLE, ACTIVE, COMPLETED } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityProducer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityProducer.java index 7e38b60d12..0b512676ba 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityProducer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicEntityProducer.java @@ -44,7 +44,10 @@ * processing is executed through an {@link Executor}. * * @since 5.0 + * + * @deprecated Use {@link ClassicToAsyncRequestProducer}. */ +@Deprecated public abstract class AbstractClassicEntityProducer implements AsyncEntityProducer { private enum State { IDLE, ACTIVE, COMPLETED } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicServerExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicServerExchangeHandler.java index 421e46d307..6d93965361 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicServerExchangeHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/AbstractClassicServerExchangeHandler.java @@ -63,7 +63,10 @@ * Blocking input / output processing is executed through an {@link Executor}. * * @since 5.0 + * + * @deprecated Use {@link ClassicToAsyncServerExchangeHandler}. */ +@Deprecated public abstract class AbstractClassicServerExchangeHandler implements AsyncServerExchangeHandler { private enum State { IDLE, ACTIVE, COMPLETED } @@ -114,7 +117,7 @@ public final void handleRequest( final AtomicBoolean responseCommitted = new AtomicBoolean(); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); - final HttpResponse responseWrapper = new HttpResponseWrapper(response){ + final HttpResponse responseWrapper = new HttpResponseWrapper(response) { private void ensureNotCommitted() { Asserts.check(!responseCommitted.get(), "Response already committed"); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncRequestProducer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncRequestProducer.java new file mode 100644 index 0000000000..a1959d14de --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncRequestProducer.java @@ -0,0 +1,196 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.nio.support.classic; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.RequestChannel; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; +import org.apache.hc.core5.util.Timeout; + +/** + * @since 5.4 + */ +@Experimental +public class ClassicToAsyncRequestProducer implements AsyncRequestProducer { + + private final ClassicHttpRequest request; + private final int initialBufferSize; + private final Timeout timeout; + private final CountDownLatch countDownLatch; + private final AtomicReference bufferRef; + private final AtomicReference exceptionRef; + + private volatile boolean repeatable; + + public interface IORunnable { + + void execute() throws IOException; + + } + + public ClassicToAsyncRequestProducer(final ClassicHttpRequest request, final int initialBufferSize, final Timeout timeout) { + this.request = Args.notNull(request, "HTTP request"); + this.initialBufferSize = Args.positive(initialBufferSize, "Initial buffer size"); + this.timeout = timeout; + this.countDownLatch = new CountDownLatch(1); + this.bufferRef = new AtomicReference<>(); + this.exceptionRef = new AtomicReference<>(); + } + + public ClassicToAsyncRequestProducer(final ClassicHttpRequest request, final Timeout timeout) { + this(request, ClassicToAsyncSupport.INITIAL_BUF_SIZE, timeout); + } + + void propagateException() throws IOException { + final Exception ex = exceptionRef.getAndSet(null); + if (ex != null) { + ClassicToAsyncSupport.rethrow(ex); + } + } + + public IORunnable blockWaiting() throws IOException, InterruptedException { + if (timeout == null) { + countDownLatch.await(); + } else { + if (!countDownLatch.await(timeout.getDuration(), timeout.getTimeUnit())) { + throw new InterruptedIOException("Timeout blocked waiting for output (" + timeout + ")"); + } + } + propagateException(); + final SharedOutputBuffer outputBuffer = bufferRef.get(); + return () -> { + final HttpEntity requestEntity = request.getEntity(); + if (requestEntity != null) { + try (final InternalOutputStream outputStream = new InternalOutputStream(outputBuffer)) { + requestEntity.writeTo(outputStream); + } + } + }; + } + + @Override + public void sendRequest(final RequestChannel channel, final HttpContext context) throws HttpException, IOException { + final HttpEntity requestEntity = request.getEntity(); + final SharedOutputBuffer buffer = requestEntity != null ? new SharedOutputBuffer(initialBufferSize) : null; + bufferRef.set(buffer); + repeatable = requestEntity == null || requestEntity.isRepeatable(); + channel.sendRequest(request, requestEntity, null); + countDownLatch.countDown(); + } + + @Override + public boolean isRepeatable() { + return repeatable; + } + + @Override + public int available() { + final SharedOutputBuffer buffer = bufferRef.get(); + if (buffer != null) { + return buffer.length(); + } + return 0; + } + + @Override + public void produce(final DataStreamChannel channel) throws IOException { + final SharedOutputBuffer buffer = bufferRef.get(); + if (buffer != null) { + buffer.flush(channel); + } + } + + @Override + public void failed(final Exception cause) { + try { + exceptionRef.set(cause); + } finally { + countDownLatch.countDown(); + } + } + + @Override + public void releaseResources() { + } + + class InternalOutputStream extends OutputStream { + + private final SharedOutputBuffer buffer; + + public InternalOutputStream(final SharedOutputBuffer buffer) { + Asserts.notNull(buffer, "Shared buffer"); + this.buffer = buffer; + } + + @Override + public void close() throws IOException { + propagateException(); + this.buffer.writeCompleted(timeout); + } + + @Override + public void flush() throws IOException { + propagateException(); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + propagateException(); + this.buffer.write(b, off, len, timeout); + } + + @Override + public void write(final byte[] b) throws IOException { + propagateException(); + if (b == null) { + return; + } + this.buffer.write(b, 0, b.length, timeout); + } + + @Override + public void write(final int b) throws IOException { + propagateException(); + this.buffer.write(b, timeout); + } + + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncResponseConsumer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncResponseConsumer.java new file mode 100644 index 0000000000..179eabf214 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncResponseConsumer.java @@ -0,0 +1,330 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.nio.support.classic; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; +import org.apache.hc.core5.http.io.support.ClassicResponseBuilder; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.CapacityChannel; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.io.Closer; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; +import org.apache.hc.core5.util.Timeout; + +/** + * @since 5.4 + */ +@Experimental +public class ClassicToAsyncResponseConsumer implements AsyncResponseConsumer { + + static class ResponseData { + + final HttpResponse head; + final EntityDetails entityDetails; + + ResponseData(final HttpResponse head, + final EntityDetails entityDetails) { + this.head = head; + this.entityDetails = entityDetails; + } + + } + + private final int initialBufferSize; + private final Timeout timeout; + private final CountDownLatch countDownLatch; + private final AtomicReference responseRef; + private final AtomicReference> callbackRef; + private final AtomicReference bufferRef; + private final AtomicReference exceptionRef; + + public ClassicToAsyncResponseConsumer(final int initialBufferSize, final Timeout timeout) { + this.initialBufferSize = Args.positive(initialBufferSize, "Initial buffer size"); + this.timeout = timeout; + this.countDownLatch = new CountDownLatch(1); + this.responseRef = new AtomicReference<>(); + this.callbackRef = new AtomicReference<>(); + this.bufferRef = new AtomicReference<>(); + this.exceptionRef = new AtomicReference<>(); + } + + public ClassicToAsyncResponseConsumer(final Timeout timeout) { + this(ClassicToAsyncSupport.INITIAL_BUF_SIZE, timeout); + } + + void propagateException() throws IOException { + final Exception ex = exceptionRef.getAndSet(null); + if (ex != null) { + ClassicToAsyncSupport.rethrow(ex); + } + } + + void fireComplete() throws IOException { + final FutureCallback callback = callbackRef.getAndSet(null); + if (callback != null) { + callback.completed(null); + } + } + + public ClassicHttpResponse blockWaiting() throws IOException, InterruptedException { + if (timeout == null) { + countDownLatch.await(); + } else { + if (!countDownLatch.await(timeout.getDuration(), timeout.getTimeUnit())) { + throw new InterruptedIOException("Timeout blocked waiting for input (" + timeout + ")"); + } + } + propagateException(); + final ResponseData r = responseRef.getAndSet(null); + Asserts.notNull(r, "HTTP response is missing"); + final SharedInputBuffer inputBuffer = bufferRef.get(); + return ClassicResponseBuilder.copy(r.head) + .setEntity(r.entityDetails != null ? + new IncomingHttpEntity(new InternalInputStream(inputBuffer), r.entityDetails) : + null) + .build(); + } + + @Override + public void consumeResponse(final HttpResponse asyncResponse, + final EntityDetails entityDetails, + final HttpContext context, + final FutureCallback resultCallback) throws HttpException, IOException { + callbackRef.set(resultCallback); + final ResponseData responseData = new ResponseData(asyncResponse, entityDetails); + responseRef.set(responseData); + if (entityDetails != null) { + bufferRef.set(new SharedInputBuffer(initialBufferSize)); + } else { + fireComplete(); + } + countDownLatch.countDown(); + } + + @Override + public void informationResponse(final HttpResponse response, + final HttpContext context) throws HttpException, IOException { + } + + @Override + public final void updateCapacity(final CapacityChannel capacityChannel) throws IOException { + final SharedInputBuffer buffer = bufferRef.get(); + if (buffer != null) { + buffer.updateCapacity(capacityChannel); + } + } + + @Override + public final void consume(final ByteBuffer src) throws IOException { + final SharedInputBuffer buffer = bufferRef.get(); + if (buffer != null) { + buffer.fill(src); + } + } + + @Override + public final void streamEnd(final List trailers) throws HttpException, IOException { + final SharedInputBuffer buffer = bufferRef.get(); + if (buffer != null) { + buffer.markEndStream(); + } + } + + @Override + public final void failed(final Exception cause) { + try { + exceptionRef.set(cause); + } finally { + countDownLatch.countDown(); + } + } + + @Override + public void releaseResources() { + if (countDownLatch.getCount() > 0) { + countDownLatch.countDown(); + } + } + + class InternalInputStream extends InputStream { + + private final SharedInputBuffer buffer; + + InternalInputStream(final SharedInputBuffer buffer) { + super(); + Args.notNull(buffer, "Input buffer"); + this.buffer = buffer; + } + + @Override + public int available() throws IOException { + propagateException(); + return this.buffer.length(); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + propagateException(); + if (len == 0) { + return 0; + } + final int bytesRead = this.buffer.read(b, off, len, timeout); + if (bytesRead == -1) { + fireComplete(); + } + return bytesRead; + } + + @Override + public int read(final byte[] b) throws IOException { + propagateException(); + if (b == null) { + return 0; + } + final int bytesRead = this.buffer.read(b, 0, b.length, timeout); + if (bytesRead == -1) { + fireComplete(); + } + return bytesRead; + } + + @Override + public int read() throws IOException { + propagateException(); + final int b = this.buffer.read(timeout); + if (b == -1) { + fireComplete(); + } + return b; + } + + @Override + public void close() throws IOException { + // read and discard the remainder of the message + final byte[] tmp = new byte[1024]; + do { + /* empty */ + } while (read(tmp) >= 0); + super.close(); + } + + } + + static class IncomingHttpEntity implements HttpEntity { + + private final InputStream content; + private final EntityDetails entityDetails; + + IncomingHttpEntity(final InputStream content, final EntityDetails entityDetails) { + this.content = content; + this.entityDetails = entityDetails; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public boolean isChunked() { + return entityDetails.isChunked(); + } + + @Override + public long getContentLength() { + return entityDetails.getContentLength(); + } + + @Override + public String getContentType() { + return entityDetails.getContentType(); + } + + @Override + public String getContentEncoding() { + return entityDetails.getContentEncoding(); + } + + @Override + public InputStream getContent() throws IOException, IllegalStateException { + return content; + } + + @Override + public boolean isStreaming() { + return content != null; + } + + @Override + public void writeTo(final OutputStream outStream) throws IOException { + AbstractHttpEntity.writeTo(this, outStream); + } + + @Override + public Supplier> getTrailers() { + return null; + } + + @Override + public Set getTrailerNames() { + return Collections.emptySet(); + } + + @Override + public void close() throws IOException { + Closer.close(content); + } + + @Override + public String toString() { + return entityDetails.toString(); + } + + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncServerExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncServerExchangeHandler.java new file mode 100644 index 0000000000..b92a0ea5e9 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncServerExchangeHandler.java @@ -0,0 +1,380 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.nio.support.classic; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.function.Callback; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestMapper; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.impl.io.support.IncomingHttpEntity; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.HttpServerRequestHandler; +import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.nio.AsyncResponseProducer; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.CapacityChannel; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.ResponseChannel; +import org.apache.hc.core5.http.nio.support.BasicResponseProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.support.BasicResponseBuilder; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; + +/** + * {@link AsyncServerExchangeHandler} implementation that acts as a compatibility + * layer for classic {@link InputStream} / {@link OutputStream} based interfaces. + * Blocking input / output processing is executed through an {@link Executor}. + * + * @since 5.4 + */ +@Experimental +public class ClassicToAsyncServerExchangeHandler implements AsyncServerExchangeHandler { + + private final int initialBufferSize; + private final Executor executor; + private final HttpServerRequestHandler requestHandler; + private final Callback exceptionCallback; + private final AtomicBoolean responseCommitted; + private final AtomicReference responseProducerRef; + private final AtomicReference inputBufferRef; + private final AtomicReference outputBufferRef; + private final AtomicReference exceptionRef; + + public ClassicToAsyncServerExchangeHandler( + final int initialBufferSize, + final Executor executor, + final HttpServerRequestHandler requestHandler, + final Callback exceptionCallback) { + this.initialBufferSize = Args.positive(initialBufferSize, "Initial buffer size"); + this.executor = Args.notNull(executor, "Executor"); + this.requestHandler = Args.notNull(requestHandler, "Request handler"); + this.exceptionCallback = exceptionCallback; + this.responseCommitted = new AtomicBoolean(); + this.responseProducerRef = new AtomicReference<>(); + this.inputBufferRef = new AtomicReference<>(); + this.outputBufferRef = new AtomicReference<>(); + this.exceptionRef = new AtomicReference<>(); + } + + public ClassicToAsyncServerExchangeHandler( + final Executor executor, + final HttpServerRequestHandler requestHandler, + final Callback exceptionCallback) { + this(ClassicToAsyncSupport.INITIAL_BUF_SIZE, executor, requestHandler, exceptionCallback); + } + + public ClassicToAsyncServerExchangeHandler( + final Executor executor, + final HttpRequestMapper handlerMapper, + final Callback exceptionCallback) { + this(ClassicToAsyncSupport.INITIAL_BUF_SIZE, executor, + new BasicHttpServerRequestHandler(handlerMapper), + exceptionCallback); + } + + public ClassicToAsyncServerExchangeHandler( + final Executor executor, + final HttpRequestHandler handler, + final Callback exceptionCallback) { + this(ClassicToAsyncSupport.INITIAL_BUF_SIZE, executor, + new BasicHttpServerRequestHandler((request, context) -> handler), + exceptionCallback); + } + + void propagateException() throws IOException { + final Exception ex = exceptionRef.getAndSet(null); + if (ex != null) { + ClassicToAsyncSupport.rethrow(ex); + } + } + + SharedInputBuffer inputBuffer() { + final SharedInputBuffer inputBuffer = inputBufferRef.get(); + Asserts.notNull(inputBuffer, "Input buffer"); + return inputBuffer; + } + + SharedOutputBuffer outputBuffer() { + final SharedOutputBuffer outputBuffer = outputBufferRef.get(); + Asserts.notNull(outputBuffer, "Output buffer"); + return outputBuffer; + } + + void abortInput() { + final SharedInputBuffer inputBuffer = inputBufferRef.get(); + if (inputBuffer != null) { + inputBuffer.abort(); + } + } + + void abortOutput() { + final SharedOutputBuffer outputBuffer = outputBufferRef.get(); + if (outputBuffer != null) { + outputBuffer.abort(); + } + } + + @Override + public final void handleRequest( + final HttpRequest request, + final EntityDetails entityDetails, + final ResponseChannel responseChannel, + final HttpContext context) throws HttpException, IOException { + if (entityDetails != null) { + final SharedInputBuffer inputBuffer = new SharedInputBuffer(initialBufferSize); + inputBufferRef.set(inputBuffer); + } + executor.execute(() -> { + try { + final ClassicHttpRequest cr = ClassicRequestBuilder.copy(request).build(); + if (entityDetails != null) { + cr.setEntity(new IncomingHttpEntity( + new InternalInputStream(inputBufferRef.get()), + entityDetails.getContentLength(), + request)); + } + + final HttpServerRequestHandler.ResponseTrigger trigger = new HttpServerRequestHandler.ResponseTrigger() { + + @Override + public void sendInformation(final ClassicHttpResponse response) throws HttpException, IOException { + responseChannel.sendInformation(response, context); + } + + @Override + public void submitResponse(final ClassicHttpResponse response) throws HttpException, IOException { + if (responseCommitted.compareAndSet(false, true)) { + final HttpEntity responseEntity = response.getEntity(); + final String method = request.getMethod(); + final boolean contentExpected = responseEntity != null && !Method.HEAD.isSame(method); + if (contentExpected) { + final SharedOutputBuffer outputBuffer = new SharedOutputBuffer(initialBufferSize); + outputBufferRef.set(outputBuffer); + } + responseChannel.sendResponse(response, responseEntity, null); + if (contentExpected) { + responseEntity.writeTo(new InternalOutputStream(outputBufferRef.get())); + } + } else { + throw new IllegalStateException("Response has already been committed"); + } + } + + }; + try { + requestHandler.handle(cr, trigger, context); + } catch (HttpException | RuntimeException ex) { + if (responseCommitted.compareAndSet(false, true)) { + final AsyncResponseProducer responseProducer = handleError(ex); + responseProducerRef.set(responseProducer); + responseProducer.sendResponse(responseChannel, context); + } else { + throw ex; + } + } + } catch (final Exception ex) { + if (exceptionCallback != null) { + exceptionCallback.execute(ex); + } + responseChannel.terminateExchange(); + } + }); + } + + protected AsyncResponseProducer handleError(final Exception ex) { + final int status = (ex instanceof ProtocolException) ? HttpStatus.SC_BAD_REQUEST : HttpStatus.SC_INTERNAL_SERVER_ERROR; + return new BasicResponseProducer( + BasicResponseBuilder.create(status).build(), + ex.getMessage(), + ContentType.TEXT_PLAIN); + } + + @Override + public final void updateCapacity(final CapacityChannel capacityChannel) throws IOException { + inputBuffer().updateCapacity(capacityChannel); + } + + @Override + public final void consume(final ByteBuffer src) throws IOException { + inputBuffer().fill(src); + } + + @Override + public final void streamEnd(final List trailers) throws HttpException, IOException { + inputBuffer().markEndStream(); + } + + @Override + public final int available() { + final AsyncResponseProducer responseProducer = responseProducerRef.get(); + if (responseProducer != null) { + return responseProducer.available(); + } else { + return outputBuffer().length(); + } + } + + @Override + public final void produce(final DataStreamChannel channel) throws IOException { + final AsyncResponseProducer responseProducer = responseProducerRef.get(); + if (responseProducer != null) { + responseProducer.produce(channel); + } else { + outputBuffer().flush(channel); + } + } + + @Override + public final void failed(final Exception cause) { + responseCommitted.set(true); + exceptionRef.compareAndSet(null, cause); + abortInput(); + abortOutput(); + } + + @Override + public void releaseResources() { + } + + class InternalInputStream extends InputStream { + + private final ContentInputBuffer buffer; + + InternalInputStream(final ContentInputBuffer buffer) { + super(); + Args.notNull(buffer, "Input buffer"); + this.buffer = buffer; + } + + @Override + public int available() throws IOException { + propagateException(); + return buffer.length(); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + propagateException(); + if (len == 0) { + return 0; + } + return buffer.read(b, off, len); + } + + @Override + public int read(final byte[] b) throws IOException { + propagateException(); + if (b == null) { + return 0; + } + return buffer.read(b, 0, b.length); + } + + @Override + public int read() throws IOException { + propagateException(); + return buffer.read(); + } + + @Override + public void close() throws IOException { + propagateException(); + // read and discard the remainder of the message + final byte[] tmp = new byte[1024]; + do { + /* empty */ + } while (read(tmp) >= 0); + super.close(); + } + + } + + class InternalOutputStream extends OutputStream { + + private final SharedOutputBuffer buffer; + + public InternalOutputStream(final SharedOutputBuffer buffer) { + Asserts.notNull(buffer, "Shared buffer"); + this.buffer = buffer; + } + + @Override + public void close() throws IOException { + propagateException(); + buffer.writeCompleted(); + } + + @Override + public void flush() throws IOException { + propagateException(); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + propagateException(); + buffer.write(b, off, len); + } + + @Override + public void write(final byte[] b) throws IOException { + propagateException(); + if (b == null) { + return; + } + buffer.write(b, 0, b.length); + } + + @Override + public void write(final int b) throws IOException { + propagateException(); + buffer.write(b); + } + + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncSupport.java new file mode 100644 index 0000000000..30b0fa696a --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ClassicToAsyncSupport.java @@ -0,0 +1,56 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http.nio.support.classic; + +import java.io.IOException; + +import org.apache.hc.core5.http.ConnectionClosedException; +import org.apache.hc.core5.http.HttpException; + +final class ClassicToAsyncSupport { + + final static int INITIAL_BUF_SIZE = 2048; + + static void rethrow(final Throwable ex) throws IOException { + if (ex instanceof Error) { + throw (Error) ex; + } else if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } else if (ex instanceof ConnectionClosedException) { + throw (ConnectionClosedException) ex; + } else if (ex instanceof IOException) { + throw new TransportException((IOException) ex); + } else if (ex instanceof HttpException) { + throw new ProtocolException((HttpException) ex); + } else { + // Unexpected exception type + throw new IllegalStateException(ex); + } + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ContentInputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ContentInputStream.java index 0df79a5dc0..06b6d26fa9 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ContentInputStream.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ContentInputStream.java @@ -78,6 +78,7 @@ public void close() throws IOException { // read and discard the remainder of the message final byte[] tmp = new byte[1024]; while (this.buffer.read(tmp, 0, tmp.length) >= 0) { + // ignored } super.close(); } diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/HttpBinIT.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ProtocolException.java similarity index 63% rename from httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/HttpBinIT.java rename to httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ProtocolException.java index 688f49298a..1c76c00915 100644 --- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/compatibility/http2/HttpBinIT.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/ProtocolException.java @@ -25,29 +25,21 @@ * */ -package org.apache.hc.core5.testing.compatibility.http2; +package org.apache.hc.core5.http.nio.support.classic; -import org.apache.hc.core5.http.HttpHost; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import java.io.IOException; -class HttpBinIT { - private H2CompatibilityTest h2CompatibilityTest; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.http.HttpException; - @BeforeEach - void start() throws Exception { - h2CompatibilityTest = new H2CompatibilityTest(); - h2CompatibilityTest.start(); - } +/** + * @since 5.4 + */ +@Internal +public class ProtocolException extends IOException { - @AfterEach - void shutdown() throws Exception { - h2CompatibilityTest.shutdown(); + public ProtocolException(final HttpException ex) { + super(ex); } - @Test - void executeHttpBin() throws Exception { - h2CompatibilityTest.executeHttpBin(new HttpHost("http", "localhost", 8082)); - } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedInputBuffer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedInputBuffer.java index cb3d30245e..992f6ed015 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedInputBuffer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedInputBuffer.java @@ -35,6 +35,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.nio.CapacityChannel; +import org.apache.hc.core5.util.Timeout; /** * @since 5.0 @@ -93,12 +94,18 @@ public void updateCapacity(final CapacityChannel capacityChannel) throws IOExcep } } - private void awaitInput() throws InterruptedIOException { + private void awaitInput(final Timeout timeout) throws InterruptedIOException { if (!buffer().hasRemaining()) { setInputMode(); while (buffer().position() == 0 && !endStream && !aborted) { try { - condition.await(); + if (timeout == null) { + condition.await(); + } else { + if (!condition.await(timeout.getDuration(), timeout.getTimeUnit())) { + throw new InterruptedIOException("Timeout blocked waiting for input (" + timeout + ")"); + } + } } catch (final InterruptedException ex) { Thread.currentThread().interrupt(); throw new InterruptedIOException(ex.getMessage()); @@ -108,15 +115,26 @@ private void awaitInput() throws InterruptedIOException { } } + private void ensureNotAborted() throws InterruptedIOException { + if (aborted) { + throw new InterruptedIOException("Operation aborted"); + } + } + @Override public int read() throws IOException { + return read(null); + } + + /** + * @since 5.4 + */ + public int read(final Timeout timeout) throws IOException { lock.lock(); try { setOutputMode(); - awaitInput(); - if (aborted) { - return -1; - } + awaitInput(timeout); + ensureNotAborted(); if (!buffer().hasRemaining() && endStream) { return -1; } @@ -133,16 +151,21 @@ public int read() throws IOException { @Override public int read(final byte[] b, final int off, final int len) throws IOException { + return read(b, off, len, null); + } + + /** + * @since 5.4 + */ + public int read(final byte[] b, final int off, final int len, final Timeout timeout) throws IOException { if (len == 0) { return 0; } lock.lock(); try { setOutputMode(); - awaitInput(); - if (aborted) { - return -1; - } + awaitInput(timeout); + ensureNotAborted(); if (!buffer().hasRemaining() && endStream) { return -1; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedOutputBuffer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedOutputBuffer.java index bf25a9b787..911259b210 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedOutputBuffer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/SharedOutputBuffer.java @@ -29,11 +29,13 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.util.Timeout; /** * @since 5.0 @@ -41,14 +43,14 @@ @Contract(threading = ThreadingBehavior.SAFE) public final class SharedOutputBuffer extends AbstractSharedBuffer implements ContentOutputBuffer { + private final AtomicBoolean endStreamPropagated; private volatile DataStreamChannel dataStreamChannel; private volatile boolean hasCapacity; - private volatile boolean endStreamPropagated; public SharedOutputBuffer(final ReentrantLock lock, final int initialBufferSize) { super(lock, initialBufferSize); this.hasCapacity = false; - this.endStreamPropagated = false; + this.endStreamPropagated = new AtomicBoolean(); } public SharedOutputBuffer(final int bufferSize) { @@ -79,8 +81,10 @@ private void ensureNotAborted() throws InterruptedIOException { } } - @Override - public void write(final byte[] b, final int off, final int len) throws IOException { + /** + * @since 5.4 + */ + public void write(final byte[] b, final int off, final int len, final Timeout timeout) throws IOException { final ByteBuffer src = ByteBuffer.wrap(b, off, len); lock.lock(); try { @@ -92,13 +96,13 @@ public void write(final byte[] b, final int off, final int len) throws IOExcepti buffer().put(src); } else { if (buffer().position() > 0 || dataStreamChannel == null) { - waitFlush(); + waitFlush(timeout); } if (buffer().position() == 0 && dataStreamChannel != null) { final int bytesWritten = dataStreamChannel.write(src); if (bytesWritten == 0) { hasCapacity = false; - waitFlush(); + waitFlush(timeout); } } } @@ -109,13 +113,20 @@ public void write(final byte[] b, final int off, final int len) throws IOExcepti } @Override - public void write(final int b) throws IOException { + public void write(final byte[] b, final int off, final int len) throws IOException { + write(b, off, len, null); + } + + /** + * @since 5.4 + */ + public void write(final int b, final Timeout timeout) throws IOException { lock.lock(); try { ensureNotAborted(); setInputMode(); if (!buffer().hasRemaining()) { - waitFlush(); + waitFlush(timeout); } buffer().put((byte)b); } finally { @@ -124,7 +135,14 @@ public void write(final int b) throws IOException { } @Override - public void writeCompleted() throws IOException { + public void write(final int b) throws IOException { + write(b, null); + } + + /** + * @since 5.4 + */ + public void writeCompleted(final Timeout timeout) throws IOException { if (endStream) { return; } @@ -136,6 +154,7 @@ public void writeCompleted() throws IOException { setOutputMode(); if (buffer().hasRemaining()) { dataStreamChannel.requestOutput(); + waitEndStream(timeout); } else { propagateEndStream(); } @@ -146,28 +165,51 @@ public void writeCompleted() throws IOException { } } - private void waitFlush() throws InterruptedIOException { - setOutputMode(); + @Override + public void writeCompleted() throws IOException { + writeCompleted(null); + } + + private void waitFlush(final Timeout timeout) throws InterruptedIOException { if (dataStreamChannel != null) { dataStreamChannel.requestOutput(); } - ensureNotAborted(); + setOutputMode(); while (buffer().hasRemaining() || !hasCapacity) { - try { - condition.await(); - } catch (final InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException(ex.getMessage()); - } ensureNotAborted(); + waitForSignal(timeout); } setInputMode(); } + private void waitEndStream(final Timeout timeout) throws InterruptedIOException { + if (dataStreamChannel != null) { + dataStreamChannel.requestOutput(); + } + while (!endStreamPropagated.get() && !aborted) { + waitForSignal(timeout); + } + } + + private void waitForSignal(final Timeout timeout) throws InterruptedIOException { + try { + if (timeout == null) { + condition.await(); + } else { + if (!condition.await(timeout.getDuration(), timeout.getTimeUnit())) { + aborted = true; + throw new InterruptedIOException("Timeout blocked waiting for output (" + timeout + ")"); + } + } + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(ex.getMessage()); + } + } + private void propagateEndStream() throws IOException { - if (!endStreamPropagated) { + if (endStreamPropagated.compareAndSet(false, true)) { dataStreamChannel.endStream(); - endStreamPropagated = true; } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/TransportException.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/TransportException.java new file mode 100644 index 0000000000..4aad21a5fd --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/classic/TransportException.java @@ -0,0 +1,44 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http.nio.support.classic; + +import java.io.IOException; + +import org.apache.hc.core5.annotation.Internal; + +/** + * @since 5.4 + */ +@Internal +public class TransportException extends IOException { + + public TransportException(final IOException ex) { + super(ex); + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpContext.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpContext.java index 5dfb5d143d..ff78bdc73b 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpContext.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpContext.java @@ -50,7 +50,7 @@ public interface HttpContext { /** The prefix reserved for use by HTTP components. "http." */ - String RESERVED_PREFIX = "http."; + String RESERVED_PREFIX = "http."; /** * Returns protocol version used in this context. diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpCoreContext.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpCoreContext.java index 3097b3d33a..4a85a4a0d2 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpCoreContext.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpCoreContext.java @@ -54,7 +54,7 @@ public class HttpCoreContext implements HttpContext { * @deprecated Use getter methods */ @Deprecated - public static final String CONNECTION_ENDPOINT = HttpContext.RESERVED_PREFIX + "connection-endpoint"; + public static final String CONNECTION_ENDPOINT = HttpContext.RESERVED_PREFIX + "connection-endpoint"; /** * @deprecated Use getter methods @@ -66,13 +66,13 @@ public class HttpCoreContext implements HttpContext { * @deprecated Use getter methods */ @Deprecated - public static final String HTTP_REQUEST = HttpContext.RESERVED_PREFIX + "request"; + public static final String HTTP_REQUEST = HttpContext.RESERVED_PREFIX + "request"; /** * @deprecated Use getter methods */ @Deprecated - public static final String HTTP_RESPONSE = HttpContext.RESERVED_PREFIX + "response"; + public static final String HTTP_RESPONSE = HttpContext.RESERVED_PREFIX + "response"; public static HttpCoreContext create() { return new HttpCoreContext(); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpDateGenerator.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpDateGenerator.java index 469dd17a4d..464de51de6 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpDateGenerator.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/HttpDateGenerator.java @@ -79,7 +79,7 @@ private HttpDateGenerator(final String pattern, final ZoneId zoneId) { .parseCaseInsensitive() .appendPattern(pattern) .toFormatter(); - this.zoneId = zoneId; + this.zoneId = zoneId; this.lock = new ReentrantLock(); } @@ -97,5 +97,4 @@ public String getCurrentDate() { lock.unlock(); } } - } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestContent.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestContent.java index 7654b8985b..24ee1f1641 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestContent.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestContent.java @@ -136,9 +136,11 @@ public void process(final HttpRequest request, final EntityDetails entity, final MessageSupport.addContentEncodingHeader(request, entity); } } + private boolean isContentEnclosingMethod(final String method) { - return (Method.POST.isSame(method)||Method.PUT.isSame(method)||Method.PATCH.isSame(method)); + return Method.POST.isSame(method) || Method.PUT.isSame(method) || Method.PATCH.isSame(method) || Method.QUERY.isSame(method); } + /** * Validates the presence of the Content-Type header for an OPTIONS request. * diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java new file mode 100644 index 0000000000..dcf6cd7196 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/RequestTE.java @@ -0,0 +1,120 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http.protocol; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.message.MessageSupport; +import org.apache.hc.core5.util.Args; + + +/** + * HTTP protocol interceptor responsible for validating and processing the {@link HttpHeaders#TE} header field in HTTP/1.1 requests. + *

    + * The {@link HttpHeaders#TE} header is used to indicate transfer codings the client is willing to accept and, in some cases, whether + * the client is willing to accept trailer fields. This interceptor ensures that the {@link HttpHeaders#TE} header does not include + * the {@code chunked} transfer coding and validates the presence of the {@code Connection: TE} header. + *

    + * For HTTP/1.1 requests, the {@link HttpHeaders#TE} header can contain multiple values separated by commas and may include quality + * values (denoted by {@code q=}) separated by semicolons. + *

    + * In case of HTTP/2, this validation is skipped, and another layer of logic handles the specifics of HTTP/2 compliance. + * + * @since 5.4 + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class RequestTE implements HttpRequestInterceptor { + + /** + * Singleton instance of the {@code RequestTE} interceptor. + */ + public static final HttpRequestInterceptor INSTANCE = new RequestTE(); + + /** + * Default constructor. + */ + public RequestTE() { + super(); + } + + /** + * Processes the {@code TE} header of the given HTTP request and ensures compliance with HTTP/1.1 requirements. + *

    + * If the {@code TE} header is present, this method validates that: + *

      + *
    • The {@code TE} header does not include the {@code chunked} transfer coding, which is implicitly supported for HTTP/1.1.
    • + *
    • The {@code Connection} header includes the {@code TE} directive, as required by the protocol.
    • + *
    + * + * @param request the HTTP request containing the headers to validate + * @param entity the entity associated with the request (may be {@code null}) + * @param context the execution context for the request + * @throws HttpException if the {@code TE} header contains invalid values or the {@code Connection} header is missing + * @throws IOException in case of an I/O error + */ + @Override + public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context) + throws HttpException, IOException { + Args.notNull(request, "HTTP request"); + + final AtomicBoolean hasTE = new AtomicBoolean(false); + final AtomicBoolean hasChunk = new AtomicBoolean(false); + MessageSupport.parseTokens(request, HttpHeaders.TE, token -> { + hasTE.set(true); + if (token.equalsIgnoreCase("chunked")) { + hasChunk.set(true); + } + }); + if (hasChunk.get()) { + throw new ProtocolException("'chunked' transfer coding must not be listed in the TE header for HTTP/1.1."); + } + if (hasTE.get()) { + final AtomicBoolean hasConnection = new AtomicBoolean(false); + final AtomicBoolean hasTEinConnection = new AtomicBoolean(false); + MessageSupport.parseTokens(request, HttpHeaders.CONNECTION, token -> { + hasConnection.set(true); + if ("TE".equalsIgnoreCase(token)) { + hasTEinConnection.set(true); + } + }); + if (!hasTEinConnection.get()) { + throw new ProtocolException("The 'Connection' header must include the 'TE' directive when the 'TE' header is present."); + } + } + } + +} \ No newline at end of file diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ResponseDate.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ResponseDate.java index 9102864820..0477b54540 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ResponseDate.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/ResponseDate.java @@ -36,7 +36,6 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpResponseInterceptor; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.util.Args; /** @@ -46,27 +45,49 @@ * This interceptor is recommended for the HTTP protocol conformance and * the correct operation of the server-side message processing pipeline. *

    + *

    + * If the {@code Date} header is missing or considered invalid, and the + * {@code alwaysReplace} flag is set to {@code true}, the interceptor will replace it + * with the current system date and time. + *

    * * @since 4.0 */ @Contract(threading = ThreadingBehavior.SAFE) public class ResponseDate implements HttpResponseInterceptor { + /** + * Indicates whether to always replace an invalid or missing {@code Date} header. + * + * @since 5.4 + */ + private final boolean alwaysReplace; + public static final ResponseDate INSTANCE = new ResponseDate(); public ResponseDate() { + this(false); + } + + /** + * Constructs a ResponseDate interceptor. + * + * @param alwaysReplace Whether to replace an invalid {@code Date} header. + * If {@code true}, the interceptor will replace any + * detected invalid {@code Date} header with a valid value. + * @since 5.4 + */ + public ResponseDate(final boolean alwaysReplace) { super(); + this.alwaysReplace = alwaysReplace; } @Override public void process(final HttpResponse response, final EntityDetails entity, final HttpContext context) throws HttpException, IOException { Args.notNull(response, "HTTP response"); - final int status = response.getCode(); - if ((status >= HttpStatus.SC_OK) && - !response.containsHeader(HttpHeaders.DATE)) { + if (alwaysReplace || response.getFirstHeader(HttpHeaders.DATE) == null) { response.setHeader(HttpHeaders.DATE, HttpDateGenerator.INSTANCE.getCurrentDate()); } } - } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/UriPatternMatcher.java b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/UriPatternMatcher.java index ac8ab6d099..bb49fbdf0c 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/UriPatternMatcher.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/protocol/UriPatternMatcher.java @@ -149,8 +149,9 @@ public T lookup(final String path) { for (final String pattern : this.map.keySet()) { if (matchUriRequestPattern(pattern, path)) { // we have a match. is it any better? - if (bestMatch == null || (bestMatch.length() < pattern.length()) - || (bestMatch.length() == pattern.length() && pattern.endsWith("*"))) { + if (bestMatch == null + || bestMatch.length() < pattern.length() + || bestMatch.length() == pattern.length() && pattern.endsWith("*")) { obj = this.map.get(pattern); bestMatch = pattern; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/ssl/TLS.java b/httpcore5/src/main/java/org/apache/hc/core5/http/ssl/TLS.java index 8268eb5b53..27d18fce83 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/ssl/TLS.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/ssl/TLS.java @@ -43,7 +43,7 @@ */ public enum TLS { - V_1_0("TLSv1", new ProtocolVersion("TLS", 1, 0)), + V_1_0("TLSv1", new ProtocolVersion("TLS", 1, 0)), V_1_1("TLSv1.1", new ProtocolVersion("TLS", 1, 1)), V_1_2("TLSv1.2", new ProtocolVersion("TLS", 1, 2)), V_1_3("TLSv1.3", new ProtocolVersion("TLS", 1, 3)); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java index bdc0cdb850..2d26205108 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/support/BasicRequestBuilder.java @@ -33,6 +33,7 @@ import java.util.Arrays; import java.util.List; +import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; @@ -104,6 +105,44 @@ public static BasicRequestBuilder head(final String uri) { return new BasicRequestBuilder(Method.HEAD, uri); } + /** + * Initializes a new {@link BasicRequestBuilder} instance for the {@code QUERY} method. + * + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static BasicRequestBuilder query() { + return new BasicRequestBuilder(Method.QUERY); + } + + /** + * Initializes a new {@link BasicRequestBuilder} instance for the {@code QUERY} method. + * + * @param uri the request URI. + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static BasicRequestBuilder query(final URI uri) { + return new BasicRequestBuilder(Method.QUERY, uri); + } + + /** + * Initializes a new {@link BasicRequestBuilder} instance for the {@code QUERY} method. + * + * @param uri the request URI. + * @see Method#QUERY for more information regarding the properties of the {@code QUERY} method. + * + * @since 5.4 + */ + @Experimental + public static BasicRequestBuilder query(final String uri) { + return new BasicRequestBuilder(Method.QUERY, uri); + } + public static BasicRequestBuilder patch() { return new BasicRequestBuilder(Method.PATCH); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/io/IOFunction.java b/httpcore5/src/main/java/org/apache/hc/core5/io/IOFunction.java new file mode 100644 index 0000000000..5cf56b86a8 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/io/IOFunction.java @@ -0,0 +1,52 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.io; + +import java.io.IOException; + +/** + * Minimal equivalent of {@link java.util.function.Function} whose + * {@link #apply(Object)} method is allowed to throw {@link IOException}. + * + * @param input type + * @param result type + * @since 5.4 + */ +@FunctionalInterface +public interface IOFunction { + + /** + * Applies the transformation. + * + * @param value source value (never {@code null}) + * @return transformed value + * @throws IOException if the transformation cannot be performed + */ + R apply(T value) throws IOException; + +} \ No newline at end of file diff --git a/httpcore5/src/main/java/org/apache/hc/core5/io/SocketSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/io/SocketSupport.java deleted file mode 100644 index aaf6d8cbd7..0000000000 --- a/httpcore5/src/main/java/org/apache/hc/core5/io/SocketSupport.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.core5.io; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.net.SocketOption; - -import org.apache.hc.core5.annotation.Internal; - -/** - * @since 5.3 - */ -@Internal -public class SocketSupport { - - public static final String TCP_KEEPIDLE = "TCP_KEEPIDLE"; - public static final String TCP_KEEPINTERVAL = "TCP_KEEPINTERVAL"; - public static final String TCP_KEEPCOUNT = "TCP_KEEPCOUNT"; - - @SuppressWarnings("unchecked") - public static SocketOption getExtendedSocketOptionOrNull(final String fieldName) { - try { - final Class extendedSocketOptionsClass = Class.forName("jdk.net.ExtendedSocketOptions"); - final Field field = extendedSocketOptionsClass.getField(fieldName); - return (SocketOption) field.get(null); - } catch (final Exception ignore) { - return null; - } - } - - /** - * Object can be ServerSocket or Socket. - * - * @param ServerSocket or Socket. - * @throws IOException in case of an I/O error. - */ - public static void setOption(final T object, final String fieldName, final T value) throws IOException { - try { - final Class serverSocketClass = object.getClass(); - final Method setOptionMethod = serverSocketClass.getMethod("setOption", SocketOption.class, Object.class); - final SocketOption socketOption = getExtendedSocketOptionOrNull(fieldName); - if (socketOption == null) { - throw new UnsupportedOperationException("Extended socket option not supported: " + fieldName); - } - setOptionMethod.invoke(object, socketOption, value); - } catch (final UnsupportedOperationException e) { - throw e; - } catch (final Exception ex) { - throw new IOException("Failure setting extended socket option", ex); - } - } - -} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java b/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java index 52b8b796e9..801fddde76 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/Host.java @@ -55,12 +55,21 @@ static boolean isPunyCode(final CharSequence s) { if (s == null || s.length() < 4) { return false; } - return ((s.charAt(0) == 'x' || s.charAt(0) == 'X') && + return (s.charAt(0) == 'x' || s.charAt(0) == 'X') && (s.charAt(1) == 'n' || s.charAt(1) == 'N') && s.charAt(2) == '-' && - s.charAt(3) == '-'); + s.charAt(3) == '-'; } + /** + * Constructs a new instance. + * + * @param name The host name, not null. + * @param port The port value, between 0 and 65535, inclusive. {@code -1} indicates the scheme default port. + * @throws NullPointerException if the {@code name} is {@code null}. + * @throws IllegalArgumentException If the port parameter is outside the specified range of valid port values, which is between 0 and 65535, inclusive. + * {@code -1} indicates the scheme default port. + */ public Host(final String name, final int port) { super(); Args.notNull(name, "Host name"); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/InetAddressUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/net/InetAddressUtils.java index f141e6b96c..f9543f9f6d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/InetAddressUtils.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/InetAddressUtils.java @@ -101,6 +101,10 @@ private InetAddressUtils() { private static final int MAX_COLON_COUNT = 7; /** + * Tests whether the parameter is a valid IPv4 address + * + * @param input the address character sequence to check for validity + * @return true if the input parameter is a valid IPv4 address * @deprecated Use {@link #isIPv4(CharSequence)} */ @Deprecated @@ -109,7 +113,7 @@ public static boolean isIPv4Address(final String input) { } /** - * Checks whether the parameter is a valid IPv4 address + * Tests whether the parameter is a valid IPv4 address. * * @param input the address character sequence to check for validity * @return true if the input parameter is a valid IPv4 address @@ -120,6 +124,10 @@ public static boolean isIPv4(final CharSequence input) { } /** + * Tests if an IPv6 address is an IPv4-mapped IPv6 address. + * + * @param input the IPv6 address to be checked + * @return true if the IPv6 address is an IPv4-mapped IPv6 address, false otherwise. * @deprecated Use {@link #isIPv4MappedIPv6(CharSequence)} */ @Deprecated @@ -128,7 +136,7 @@ public static boolean isIPv4MappedIPv64Address(final String input) { } /** - * Check if an IPv6 address is an IPv4-mapped IPv6 address. + * Tests if an IPv6 address is an IPv4-mapped IPv6 address. * * @param input the IPv6 address to be checked * @return true if the IPv6 address is an IPv4-mapped IPv6 address, false otherwise. @@ -150,6 +158,10 @@ static boolean hasValidIPv6ColonCount(final CharSequence input) { } /** + * Tests whether the parameter is a valid standard (non-compressed) IPv6 address + * + * @param input the address character sequence to check for validity + * @return true if the input parameter is a valid standard (non-compressed) IPv6 address * @deprecated Use {@link #isIPv6Std(CharSequence)} */ @Deprecated @@ -158,7 +170,7 @@ public static boolean isIPv6StdAddress(final String input) { } /** - * Checks whether the parameter is a valid standard (non-compressed) IPv6 address + * Tests whether the parameter is a valid standard (non-compressed) IPv6 address * * @param input the address character sequence to check for validity * @return true if the input parameter is a valid standard (non-compressed) IPv6 address @@ -169,6 +181,10 @@ public static boolean isIPv6Std(final CharSequence input) { } /** + * Tests whether the parameter is a valid compressed IPv6 address + * + * @param input the address character sequence to check for validity + * @return true if the input parameter is a valid compressed IPv6 address * @deprecated Use {@link #isIPv6HexCompressed(CharSequence)} */ @Deprecated @@ -177,7 +193,7 @@ public static boolean isIPv6HexCompressedAddress(final String input) { } /** - * Checks whether the parameter is a valid compressed IPv6 address + * Tests whether the parameter is a valid compressed IPv6 address * * @param input the address character sequence to check for validity * @return true if the input parameter is a valid compressed IPv6 address @@ -188,6 +204,10 @@ public static boolean isIPv6HexCompressed(final CharSequence input) { } /** + * Tests whether the parameter is a valid IPv6 address (including compressed). + * + * @param input the address character sequence to check for validity + * @return true if the input parameter is a valid standard or compressed IPv6 address * @deprecated Use {@link #isIPv6(CharSequence)} */ @Deprecated @@ -196,7 +216,7 @@ public static boolean isIPv6Address(final String input) { } /** - * Checks whether the parameter is a valid IPv6 address (including compressed). + * Tests whether the parameter is a valid IPv6 address (including compressed). * * @param input the address character sequence to check for validity * @return true if the input parameter is a valid standard or compressed IPv6 address @@ -224,6 +244,11 @@ public static boolean isIPv6(final CharSequence input) { } /** + * Tests whether the parameter is a valid URL formatted bracketed IPv6 address (including compressed). + * This matches only bracketed values e.g. {@code [::1]}. + * + * @param input the address character sequence to check for validity + * @return true if the input parameter is a valid URL-formatted bracketed IPv6 address * @deprecated Use {@link #isIPv6URLBracketed(CharSequence)} */ @Deprecated @@ -232,7 +257,7 @@ public static boolean isIPv6URLBracketedAddress(final String input) { } /** - * Checks whether the parameter is a valid URL formatted bracketed IPv6 address (including compressed). + * Tests whether the parameter is a valid URL formatted bracketed IPv6 address (including compressed). * This matches only bracketed values e.g. {@code [::1]}. * * @param input the address character sequence to check for validity @@ -251,6 +276,8 @@ public static boolean isIPv6URLBracketed(final CharSequence input) { /** * Formats {@link SocketAddress} as text. * + * @param buffer The target buffer to append. + * @param socketAddress The SocketAddress to append to {@code buffer}. * @since 5.0 */ public static void formatAddress( @@ -271,14 +298,17 @@ public static void formatAddress( } /** - * Returns canonical name (fully qualified domain name) of the localhost. + * Gets the canonical name of the local host, a fully qualified domain name. + *

    + * This can be {@code "localhost"} or a fully qualified domain name like {@code "host.docker.internal"}. + *

    * + * @return the canonical name of the local host. * @since 5.0 */ public static String getCanonicalLocalHostName() { try { - final InetAddress localHost = InetAddress.getLocalHost(); - return localHost.getCanonicalHostName(); + return InetAddress.getLocalHost().getCanonicalHostName(); } catch (final UnknownHostException ex) { return "localhost"; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java index 40ebc9cb81..bdf0ec9c37 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java @@ -113,6 +113,32 @@ public class PercentCodec { RFC5987_UNRESERVED.set('~'); } + static final BitSet PCHAR = new BitSet(256); + static final BitSet USERINFO = new BitSet(256); + static final BitSet REG_NAME = new BitSet(256); + static final BitSet PATH_SEGMENT = new BitSet(256); + static final BitSet QUERY = new BitSet(256); + static final BitSet FRAGMENT = new BitSet(256); + + static { + PCHAR.or(UNRESERVED); + PCHAR.or(SUB_DELIMS); + PCHAR.set(':'); + PCHAR.set('@'); + USERINFO.or(UNRESERVED); + USERINFO.or(SUB_DELIMS); + USERINFO.set(':'); + REG_NAME.or(UNRESERVED); + REG_NAME.or(SUB_DELIMS); + PATH_SEGMENT.or(PCHAR); + QUERY.or(PCHAR); + QUERY.set('/'); + QUERY.set('?'); + FRAGMENT.or(PCHAR); + FRAGMENT.set('/'); + FRAGMENT.set('?'); + } + private static final int RADIX = 16; static void encode(final StringBuilder buf, final CharSequence content, final Charset charset, diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java index c1659bb440..86288ebac9 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java @@ -91,31 +91,51 @@ static String format(final URIAuthority uriAuthority) { /** * Constructs a new instance. * - * @throws IllegalArgumentException - * If the port parameter is outside the specified range of valid port values, which is between 0 and - * 65535, inclusive. {@code -1} indicates the scheme default port. + * @param userInfo The user info, may be null. + * @param hostName The host name, not null. + * @param port The port value, between 0 and 65535, inclusive. {@code -1} indicates the scheme default port. + * @throws NullPointerException If the {@code name} is {@code null}. + * @throws IllegalArgumentException If the port is outside the specified range of valid port values, which is between 0 and 65535, inclusive. + * {@code -1} indicates the scheme default port. */ - public URIAuthority(final String userInfo, final String hostname, final int port) { + public URIAuthority(final String userInfo, final String hostName, final int port) { super(); this.userInfo = userInfo; - this.host = new Host(hostname, port); + this.host = new Host(hostName, port); } - public URIAuthority(final String hostname, final int port) { - this(null, hostname, port); + /** + * Constructs a new instance. + * + * @param hostName The host name, not null. + * @param port The port value, between 0 and 65535, inclusive. {@code -1} indicates the scheme default port. + * @throws NullPointerException If the {@code name} is {@code null}. + * @throws IllegalArgumentException If the port is outside the specified range of valid port values, which is between 0 and 65535, inclusive. + * {@code -1} indicates the scheme default port. + */ + public URIAuthority(final String hostName, final int port) { + this(null, hostName, port); } /** + * Constructs a new instance. + * + * @param userInfo The user info, may be null. + * @param host The host, never null. + * @throws NullPointerException if the {@code host} is {@code null}. * @since 5.2 */ public URIAuthority(final String userInfo, final Host host) { super(); - Args.notNull(host, "Host"); + this.host = Args.notNull(host, "Host"); this.userInfo = userInfo; - this.host = host; } /** + * Constructs a new instance. + * + * @param host The host, never null. + * @throws NullPointerException if the {@code host} is {@code null}. * @since 5.2 */ public URIAuthority(final Host host) { @@ -123,6 +143,14 @@ public URIAuthority(final Host host) { } /** + * Constructs a new instance. + * + * @param userInfo The user info, may be null. + * @param endpoint The named end-point, never null. + * @throws NullPointerException If the end-point is {@code null}. + * @throws NullPointerException If the end-point {@code name} is {@code null}. + * @throws IllegalArgumentException If the end-point port is outside the specified range of valid port values, which is between 0 and 65535, inclusive. + * {@code -1} indicates the scheme default port. * @since 5.2 */ public URIAuthority(final String userInfo, final NamedEndpoint endpoint) { @@ -132,6 +160,15 @@ public URIAuthority(final String userInfo, final NamedEndpoint endpoint) { this.host = new Host(endpoint.getHostName(), endpoint.getPort()); } + /** + * Constructs a new instance. + * + * @param namedEndpoint The named end-point, never null. + * @throws NullPointerException If the end-point is {@code null}. + * @throws NullPointerException If the end-point {@code name} is {@code null}. + * @throws IllegalArgumentException If the end-point port is outside the specified range of valid port values, which is between 0 and 65535, inclusive. + * {@code -1} indicates the scheme default port. + */ public URIAuthority(final NamedEndpoint namedEndpoint) { this(null, namedEndpoint); } @@ -155,10 +192,21 @@ public static URIAuthority create(final String s) throws URISyntaxException { return uriAuthority; } - public URIAuthority(final String hostname) { - this(null, hostname, -1); + /** + * Constructs a new instance. + * + * @param hostName The host name, not null. + * @throws NullPointerException If the {@code name} is {@code null}. + */ + public URIAuthority(final String hostName) { + this(null, hostName, -1); } + /** + * Gets the user info String. + * + * @return the user info String. + */ public String getUserInfo() { return userInfo; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java index 048f60fd29..3925c6ff76 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java @@ -34,6 +34,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -87,6 +88,35 @@ public static URIBuilder loopbackAddress() { private Charset charset; private String fragment; private String encodedFragment; + private EncodingPolicy encodingPolicy = EncodingPolicy.ALL_RESERVED; + + private boolean plusAsBlank; + + /** + * Defines the encoding policy for URI components in {@link URIBuilder}. + * This enum controls how characters are percent-encoded when constructing a URI, + * allowing flexibility between strict encoding and RFC 3986-compliant behavior. + * + * @since 5.4 + */ + public enum EncodingPolicy { + /** + * Encodes all reserved characters, allowing only unreserved characters + * (ALPHA, DIGIT, "-", ".", "_", "~") to remain unencoded. This is a strict + * policy suitable for conservative URI production where maximum encoding + * is desired. + */ + ALL_RESERVED, + + /** + * Follows RFC 3986 component-specific encoding rules. For example, query and + * fragment components allow unreserved characters, sub-delimiters ("!", "$", + * "&", "'", "(", ")", "*", "+", ",", ";", "="), and additional characters + * (":", "@", "/", "?") to remain unencoded, as defined by {@code PercentCodec.FRAGMENT}. + * This policy ensures compliance with RFC 3986 while maintaining interoperability. + */ + RFC_3986 + } /** * Constructs an empty instance. @@ -173,6 +203,22 @@ public URIBuilder setCharset(final Charset charset) { return this; } + /** + * Sets the encoding policy for this {@link URIBuilder}. + * The encoding policy determines how URI components (e.g., query, fragment) are + * percent-encoded when building the URI string. If not set, the default policy + * is {@link EncodingPolicy#RFC_3986}. + * + * @param encodingPolicy the encoding policy to apply, or {@code null} to reset + * to the default ({@link EncodingPolicy#ALL_RESERVED}) + * @return this {@link URIBuilder} instance for method chaining + * @since 5.4 + */ + public URIBuilder setEncodingPolicy(final EncodingPolicy encodingPolicy) { + this.encodingPolicy = encodingPolicy; + return this; + } + /** * Gets the authority. * @@ -192,6 +238,33 @@ public Charset getCharset() { return charset; } + /** + * Sets whether the plus sign ('+') should be interpreted as a blank space (' ') when parsing + * the query parameters of the URI. + *

    + * In HTTP URLs, query strings may contain spaces encoded as '+' characters or as '%20'. + * This flag controls whether '+' is interpreted as a space or remains as a plus sign. + *

    + * + *

    + * If the query string was already set, calling this method will re-parse the query + * using the updated flag. This ensures that the query parameters are processed correctly + * based on the specified interpretation of the '+' character. + *

    + * + * @param plusAsBlank {@code true} to interpret '+' as a space, {@code false} to keep '+' as a literal plus sign. + * @return this {@link URIBuilder} instance for method chaining. + * @since 5.4 + */ + public URIBuilder setPlusAsBlank(final boolean plusAsBlank) { + this.plusAsBlank = plusAsBlank; + // Re-parse the query string using the updated flag + if (this.encodedQuery != null) { + this.queryParams = parseQuery(this.encodedQuery, this.charset, this.plusAsBlank); + } + return this; + } + private static final char QUERY_PARAM_SEPARATOR = '&'; private static final char PARAM_VALUE_SEPARATOR = '='; private static final char PATH_SEPARATOR = '/'; @@ -271,38 +344,51 @@ static List parsePath(final CharSequence s, final Charset charset) { return list; } - static void formatPath(final StringBuilder buf, final Iterable segments, final boolean rootless, final Charset charset) { + static void formatPath(final StringBuilder buf, final Iterable segments, final boolean rootless, + final Charset charset, final BitSet safechars) { int i = 0; for (final String segment : segments) { if (i > 0 || !rootless) { buf.append(PATH_SEPARATOR); } - PercentCodec.encode(buf, segment, charset); + PercentCodec.encode(buf, segment, charset, safechars, false); i++; } } - static void formatQuery(final StringBuilder buf, final Iterable params, final Charset charset, - final boolean blankAsPlus) { + static void formatPath(final StringBuilder buf, final Iterable segments, final boolean rootless, + final Charset charset) { + formatPath(buf, segments, rootless, charset, PercentCodec.UNRESERVED); + } + + + static void formatQuery(final StringBuilder buf, final Iterable params, + final Charset charset, final BitSet safechars, final boolean blankAsPlus) { int i = 0; for (final NameValuePair parameter : params) { if (i > 0) { buf.append(QUERY_PARAM_SEPARATOR); } - PercentCodec.encode(buf, parameter.getName(), charset, blankAsPlus); + PercentCodec.encode(buf, parameter.getName(), charset, safechars, blankAsPlus); if (parameter.getValue() != null) { buf.append(PARAM_VALUE_SEPARATOR); - PercentCodec.encode(buf, parameter.getValue(), charset, blankAsPlus); + PercentCodec.encode(buf, parameter.getValue(), charset, safechars, blankAsPlus); } i++; } } + static void formatQuery(final StringBuilder buf, final Iterable params, + final Charset charset, final boolean blankAsPlus) { + formatQuery(buf, params, charset, PercentCodec.UNRESERVED, blankAsPlus); + } + + /** * Builds a {@link URI} instance. */ public URI build() throws URISyntaxException { - if ((URIScheme.HTTPS.same(scheme) || URIScheme.HTTP.same(scheme)) && (TextUtils.isBlank(host))) { + if ((URIScheme.HTTPS.same(scheme) || URIScheme.HTTP.same(scheme)) && (TextUtils.isBlank(host))) { throw new URISyntaxException(scheme, "http/https URI cannot have an empty host identifier"); } return new URI(buildString()); @@ -327,18 +413,22 @@ private String buildString() { } else if (this.userInfo != null) { final int idx = this.userInfo.indexOf(':'); if (idx != -1) { - PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset); + PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.USERINFO, false); sb.append(':'); - PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset); + PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.USERINFO, false); } else { - PercentCodec.encode(sb, this.userInfo, this.charset); + PercentCodec.encode(sb, this.userInfo, this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.USERINFO, false); } sb.append("@"); } if (InetAddressUtils.isIPv6(this.host)) { sb.append("[").append(this.host).append("]"); } else { - sb.append(PercentCodec.encode(this.host, this.charset)); + PercentCodec.encode(sb, this.host, this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.REG_NAME, false); } if (this.port >= 0) { sb.append(":").append(this.port); @@ -353,23 +443,27 @@ private String buildString() { } sb.append(this.encodedPath); } else if (this.pathSegments != null) { - formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset); + formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.PATH_SEGMENT); } if (this.encodedQuery != null) { sb.append("?").append(this.encodedQuery); } else if (this.queryParams != null && !this.queryParams.isEmpty()) { sb.append("?"); - formatQuery(sb, this.queryParams, this.charset, false); + formatQuery(sb, this.queryParams, this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.UNRESERVED : PercentCodec.QUERY, false); } else if (this.query != null) { sb.append("?"); - PercentCodec.encode(sb, this.query, this.charset, PercentCodec.URIC, false); + PercentCodec.encode(sb, this.query, this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.URIC : PercentCodec.QUERY, false); } } if (this.encodedFragment != null) { sb.append("#").append(this.encodedFragment); } else if (this.fragment != null) { sb.append("#"); - PercentCodec.encode(sb, this.fragment, this.charset); + PercentCodec.encode(sb, this.fragment, this.charset, + encodingPolicy == EncodingPolicy.ALL_RESERVED ? PercentCodec.URIC : PercentCodec.FRAGMENT, false); } return sb.toString(); } @@ -402,7 +496,7 @@ private void digestURI(final URI uri, final Charset charset) { this.pathSegments = parsePath(uri.getRawPath(), charset); this.pathRootless = uri.getRawPath() == null || !uri.getRawPath().startsWith("/"); this.encodedQuery = uri.getRawQuery(); - this.queryParams = parseQuery(uri.getRawQuery(), charset, false); + this.queryParams = parseQuery(uri.getRawQuery(), charset, this.plusAsBlank); this.encodedFragment = uri.getRawFragment(); this.fragment = uri.getFragment(); this.charset = charset; @@ -450,7 +544,7 @@ public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final N * @return this instance. * @since 5.1 */ - public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final List nvps) { + public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final List nvps) { this.encodedSchemeSpecificPart = null; if (!TextUtils.isBlank(schemeSpecificPart)) { final StringBuilder sb = new StringBuilder(schemeSpecificPart); @@ -664,7 +758,7 @@ public URIBuilder removeQuery() { * * @return this instance. */ - public URIBuilder setParameters(final List nameValuePairs) { + public URIBuilder setParameters(final List nameValuePairs) { if (this.queryParams == null) { this.queryParams = new ArrayList<>(); } else { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java b/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java index b680ae63df..15405ac8b9 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/pool/LaxConnPool.java @@ -410,9 +410,9 @@ private PoolEntry createPoolEntry() { int prev, next; do { prev = allocated.get(); - next = (prev(route, timeToLive, disposalCallback) : null; + return prev < next ? new PoolEntry<>(route, timeToLive, disposalCallback) : null; } private void deallocatePoolEntry() { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/pool/StrictConnPool.java b/httpcore5/src/main/java/org/apache/hc/core5/pool/StrictConnPool.java index 819238b38b..0bc0c1b6dc 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/pool/StrictConnPool.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/pool/StrictConnPool.java @@ -157,12 +157,7 @@ public void close() { } private PerRoutePool getPool(final T route) { - PerRoutePool pool = this.routeToPool.get(route); - if (pool == null) { - pool = new PerRoutePool<>(route, this.disposalCallback); - this.routeToPool.put(route, pool); - } - return pool; + return this.routeToPool.computeIfAbsent(route, r -> new PerRoutePool<>(route, this.disposalCallback)); } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOReactorBase.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOReactorBase.java index 706b6a3c70..e3914f80e8 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOReactorBase.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOReactorBase.java @@ -50,13 +50,17 @@ public final Future connect( throw new IOReactorShutdownException("I/O reactor has been shut down"); } try { - return getWorkerSelector().next().connect(remoteEndpoint, remoteAddress, localAddress, timeout, attachment, callback); + final SingleCoreIOReactor dispatcher = selectWorker(); + if (dispatcher.getStatus() == IOReactorStatus.SHUT_DOWN) { + throw new IOReactorShutdownException("I/O reactor has been shut down"); + } + return dispatcher.connect(remoteEndpoint, remoteAddress, localAddress, timeout, attachment, callback); } catch (final IOReactorShutdownException ex) { initiateShutdown(); throw ex; } } - abstract IOWorkers.Selector getWorkerSelector(); + abstract SingleCoreIOReactor selectWorker(); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOSessionPool.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOSessionPool.java index 3691264aab..a7c7cffa0a 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOSessionPool.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/AbstractIOSessionPool.java @@ -38,9 +38,9 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.concurrent.CompletingFutureContribution; import org.apache.hc.core5.concurrent.ComplexFuture; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.io.CloseMode; @@ -145,14 +145,7 @@ public void completed(final IOSession ioSession) { future.completed(ioSession); } else { getSessionInternal(poolEntry, true, endpoint, connectTimeout, - new FutureContribution(future) { - - @Override - public void completed(final IOSession ioSession1) { - future.completed(ioSession1); - } - - }); + new CompletingFutureContribution<>(future)); } }); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultConnectingIOReactor.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultConnectingIOReactor.java index 1d99e52292..fb1ae47c8d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultConnectingIOReactor.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultConnectingIOReactor.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.util.concurrent.ThreadFactory; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.DefaultThreadFactory; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Decorator; @@ -48,13 +49,16 @@ */ public class DefaultConnectingIOReactor extends AbstractIOReactorBase { - private final int workerCount; private final SingleCoreIOReactor[] workers; private final MultiCoreIOReactor ioReactor; - private final IOWorkers.Selector workerSelector; + private final IOWorkerSelector workerSelector; private final static ThreadFactory THREAD_FACTORY = new DefaultThreadFactory("I/O client dispatch", true); + /** + * @since 5.4 + */ + @Internal public DefaultConnectingIOReactor( final IOEventHandlerFactory eventHandlerFactory, final IOReactorConfig ioReactorConfig, @@ -62,9 +66,11 @@ public DefaultConnectingIOReactor( final Decorator ioSessionDecorator, final Callback exceptionCallback, final IOSessionListener sessionListener, - final Callback sessionShutdownCallback) { + final IOReactorMetricsListener threadPoolListener, + final Callback sessionShutdownCallback, + final IOWorkerSelector workerSelector) { Args.notNull(eventHandlerFactory, "Event handler factory"); - this.workerCount = ioReactorConfig != null ? ioReactorConfig.getIoThreadCount() : IOReactorConfig.DEFAULT.getIoThreadCount(); + final int workerCount = ioReactorConfig != null ? ioReactorConfig.getIoThreadCount() : IOReactorConfig.DEFAULT.getIoThreadCount(); this.workers = new SingleCoreIOReactor[workerCount]; final Thread[] threads = new Thread[workerCount]; for (int i = 0; i < this.workers.length; i++) { @@ -74,12 +80,25 @@ public DefaultConnectingIOReactor( ioReactorConfig != null ? ioReactorConfig : IOReactorConfig.DEFAULT, ioSessionDecorator, sessionListener, + threadPoolListener, sessionShutdownCallback); this.workers[i] = dispatcher; threads[i] = (threadFactory != null ? threadFactory : THREAD_FACTORY).newThread(new IOReactorWorker(dispatcher)); } this.ioReactor = new MultiCoreIOReactor(this.workers, threads); - this.workerSelector = IOWorkers.newSelector(workers); + this.workerSelector = workerSelector != null ? workerSelector : IOWorkerSelectors.newSelector(workerCount); + } + + public DefaultConnectingIOReactor( + final IOEventHandlerFactory eventHandlerFactory, + final IOReactorConfig ioReactorConfig, + final ThreadFactory threadFactory, + final Decorator ioSessionDecorator, + final Callback exceptionCallback, + final IOSessionListener sessionListener, + final Callback sessionShutdownCallback) { + this(eventHandlerFactory,ioReactorConfig, threadFactory,ioSessionDecorator, exceptionCallback, sessionListener, + null, sessionShutdownCallback, null); } public DefaultConnectingIOReactor( @@ -109,8 +128,8 @@ public IOReactorStatus getStatus() { } @Override - IOWorkers.Selector getWorkerSelector() { - return workerSelector; + SingleCoreIOReactor selectWorker() { + return workers[workerSelector.select(workers)]; } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultListeningIOReactor.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultListeningIOReactor.java index fe96775116..ce99cd5924 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultListeningIOReactor.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/DefaultListeningIOReactor.java @@ -33,6 +33,7 @@ import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.DefaultThreadFactory; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.function.Callback; @@ -55,22 +56,15 @@ public class DefaultListeningIOReactor extends AbstractIOReactorBase implements private final static ThreadFactory DISPATCH_THREAD_FACTORY = new DefaultThreadFactory("I/O server dispatch", true); private final static ThreadFactory LISTENER_THREAD_FACTORY = new DefaultThreadFactory("I/O listener", true); - private final int workerCount; private final SingleCoreIOReactor[] workers; private final SingleCoreListeningIOReactor listener; private final MultiCoreIOReactor ioReactor; - private final IOWorkers.Selector workerSelector; + private final IOWorkerSelector workerSelector; /** - * Creates an instance of DefaultListeningIOReactor with the given configuration. - * - * @param eventHandlerFactory the factory to create I/O event handlers. - * @param ioReactorConfig I/O reactor configuration. - * @param listenerThreadFactory the factory to create listener thread. - * Can be {@code null}. - * - * @since 5.0 + * @since 5.4 */ + @Internal public DefaultListeningIOReactor( final IOEventHandlerFactory eventHandlerFactory, final IOReactorConfig ioReactorConfig, @@ -79,9 +73,11 @@ public DefaultListeningIOReactor( final Decorator ioSessionDecorator, final Callback exceptionCallback, final IOSessionListener sessionListener, - final Callback sessionShutdownCallback) { + final IOReactorMetricsListener threadPoolListener, + final Callback sessionShutdownCallback, + final IOWorkerSelector workerSelector) { Args.notNull(eventHandlerFactory, "Event handler factory"); - this.workerCount = ioReactorConfig != null ? ioReactorConfig.getIoThreadCount() : IOReactorConfig.DEFAULT.getIoThreadCount(); + final int workerCount = ioReactorConfig != null ? ioReactorConfig.getIoThreadCount() : IOReactorConfig.DEFAULT.getIoThreadCount(); this.workers = new SingleCoreIOReactor[workerCount]; final Thread[] threads = new Thread[workerCount + 1]; for (int i = 0; i < this.workers.length; i++) { @@ -91,19 +87,41 @@ public DefaultListeningIOReactor( ioReactorConfig != null ? ioReactorConfig : IOReactorConfig.DEFAULT, ioSessionDecorator, sessionListener, + threadPoolListener, sessionShutdownCallback); this.workers[i] = dispatcher; threads[i + 1] = (dispatchThreadFactory != null ? dispatchThreadFactory : DISPATCH_THREAD_FACTORY).newThread(new IOReactorWorker(dispatcher)); } - final IOReactor[] ioReactors = new IOReactor[this.workerCount + 1]; - System.arraycopy(this.workers, 0, ioReactors, 1, this.workerCount); + final IOReactor[] ioReactors = new IOReactor[workerCount + 1]; + System.arraycopy(this.workers, 0, ioReactors, 1, workerCount); this.listener = new SingleCoreListeningIOReactor(exceptionCallback, ioReactorConfig, this::enqueueChannel); ioReactors[0] = this.listener; threads[0] = (listenerThreadFactory != null ? listenerThreadFactory : LISTENER_THREAD_FACTORY).newThread(new IOReactorWorker(listener)); - this.ioReactor = new MultiCoreIOReactor(ioReactors, threads); + this.workerSelector = workerSelector != null ? workerSelector : IOWorkerSelectors.newSelector(workerCount); + } - workerSelector = IOWorkers.newSelector(workers); + /** + * Creates an instance of DefaultListeningIOReactor with the given configuration. + * + * @param eventHandlerFactory the factory to create I/O event handlers. + * @param ioReactorConfig I/O reactor configuration. + * @param listenerThreadFactory the factory to create listener thread. + * Can be {@code null}. + * + * @since 5.0 + */ + public DefaultListeningIOReactor( + final IOEventHandlerFactory eventHandlerFactory, + final IOReactorConfig ioReactorConfig, + final ThreadFactory dispatchThreadFactory, + final ThreadFactory listenerThreadFactory, + final Decorator ioSessionDecorator, + final Callback exceptionCallback, + final IOSessionListener sessionListener, + final Callback sessionShutdownCallback) { + this(eventHandlerFactory, ioReactorConfig, dispatchThreadFactory, listenerThreadFactory, ioSessionDecorator, + exceptionCallback, sessionListener, null, sessionShutdownCallback, null); } /** @@ -174,19 +192,18 @@ public IOReactorStatus getStatus() { } @Override - IOWorkers.Selector getWorkerSelector() { - return workerSelector; + SingleCoreIOReactor selectWorker() { + return workers[workerSelector.select(workers)]; } private void enqueueChannel(final ChannelEntry entry) { try { - workerSelector.next().enqueueChannel(entry); + selectWorker().enqueueChannel(entry); } catch (final IOReactorShutdownException ex) { initiateShutdown(); } } - @Override public void initiateShutdown() { ioReactor.initiateShutdown(); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java index d480cf72a5..87fef3d7b7 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java @@ -49,7 +49,7 @@ public final class IOReactorConfig { private final TimeValue selectInterval; private final int ioThreadCount; - private final Timeout soTimeout; + private final Timeout soTimeout; private final boolean soReuseAddress; private final TimeValue soLinger; private final boolean soKeepAlive; @@ -296,7 +296,7 @@ public static void setDefaultMaxIOThreadCount(final int defaultMaxIOThreadCount) private TimeValue selectInterval; private int ioThreadCount; - private Timeout soTimeout; + private Timeout soTimeout; private boolean soReuseAddress; private TimeValue soLinger; private boolean soKeepAlive; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorMetricsListener.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorMetricsListener.java new file mode 100644 index 0000000000..a94e675774 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorMetricsListener.java @@ -0,0 +1,71 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.reactor; + +/** + * A listener interface for receiving metrics related to the I/O reactor's + * thread pool performance and status. + * + *

    The implementing class can monitor and act upon important metrics such as + * active threads, saturation levels, and resource starvation.

    + * + * @since 5.4 + */ +public interface IOReactorMetricsListener { + + /** + * Invoked to report the current status of the thread pool, including + * active threads and pending connections. + * + * @param activeThreads The number of active threads handling connections. + * @param pendingConnections The number of pending connection requests in the queue. + */ + void onThreadPoolStatus(int activeThreads, int pendingConnections); + + /** + * Invoked to report the saturation level of the thread pool as a percentage + * of the active threads to the maximum allowed connections. + * + * @param saturationPercentage The percentage indicating thread pool saturation. + */ + void onThreadPoolSaturation(double saturationPercentage); + + /** + * Invoked when the number of pending connection requests exceeds the + * maximum allowed connections, indicating possible resource starvation. + */ + void onResourceStarvationDetected(); + + /** + * Notifies about the average wait time for connection requests in the queue. + * + * @param averageWaitTimeMillis average time in milliseconds that connection requests spend in the queue. + */ + void onQueueWaitTime(long averageWaitTimeMillis); +} + diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java index 45379c0560..9fb5a52b99 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java @@ -29,7 +29,7 @@ import java.io.IOException; import java.net.SocketAddress; -import java.net.SocketException; +import java.net.StandardSocketOptions; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.SelectionKey; @@ -131,12 +131,20 @@ public ByteChannel channel() { @Override public SocketAddress getLocalAddress() { - return this.channel.socket().getLocalSocketAddress(); + try { + return this.channel.getLocalAddress(); + } catch (final IOException e) { + return null; + } } @Override public SocketAddress getRemoteAddress() { - return this.channel.socket().getRemoteSocketAddress(); + try { + return channel.getRemoteAddress(); + } catch (final IOException e) { + return null; + } } @Override @@ -258,8 +266,8 @@ public void close(final CloseMode closeMode) { if (this.status.compareAndSet(Status.ACTIVE, Status.CLOSED)) { if (closeMode == CloseMode.IMMEDIATE) { try { - this.channel.socket().setSoLinger(true, 0); - } catch (final SocketException e) { + this.channel.setOption(StandardSocketOptions.SO_LINGER, 0); + } catch (final UnsupportedOperationException | IOException e) { // Quietly ignore } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionRequest.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionRequest.java index c808e0ac8f..57f727194f 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionRequest.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionRequest.java @@ -34,6 +34,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.concurrent.BasicFuture; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.io.CloseMode; @@ -50,6 +51,9 @@ final class IOSessionRequest implements Future { final Object attachment; final BasicFuture future; + private final long enqueueTime; + + private final AtomicReference closeableRef; public IOSessionRequest( @@ -67,6 +71,9 @@ public IOSessionRequest( this.attachment = attachment; this.future = new BasicFuture<>(callback); this.closeableRef = new AtomicReference<>(); + + // Set the time when this request is created + this.enqueueTime = System.currentTimeMillis(); } public void completed(final ProtocolIOSession ioSession) { @@ -127,4 +134,11 @@ public String toString() { ']'; } + // Getter for enqueueTime + @Internal + public long getEnqueueTime() { + return enqueueTime; + } + + } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOWorkersTest.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerSelector.java similarity index 69% rename from httpcore5/src/test/java/org/apache/hc/core5/reactor/IOWorkersTest.java rename to httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerSelector.java index 1de15a4b02..456af59299 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOWorkersTest.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerSelector.java @@ -24,21 +24,15 @@ * . * */ -package org.apache.hc.core5.reactor; -import static org.mockito.Mockito.mock; +package org.apache.hc.core5.reactor; -import org.junit.jupiter.api.Test; +import org.apache.hc.core5.annotation.Internal; -class IOWorkersTest { +@Internal +@FunctionalInterface +public interface IOWorkerSelector { - @Test - void testIndexOverflow() { - final SingleCoreIOReactor reactor = new SingleCoreIOReactor(null, mock(IOEventHandlerFactory.class), IOReactorConfig.DEFAULT, null, null, null); - final IOWorkers.Selector selector = IOWorkers.newSelector(new SingleCoreIOReactor[]{reactor, reactor, reactor}); - for (long i = Integer.MAX_VALUE - 10; i < (long) Integer.MAX_VALUE + 10; i++) { - selector.next(); - } - } + int select(IOWorkerStats[] dispatchers); } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerSelectors.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerSelectors.java new file mode 100644 index 0000000000..9170db5e9a --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerSelectors.java @@ -0,0 +1,75 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.reactor; + +import java.util.concurrent.atomic.AtomicInteger; + +final class IOWorkerSelectors { + + static IOWorkerSelector newSelector(final int workerCount, final int start) { + return isPowerOfTwo(workerCount) ? new PowerOfTwoSelector(start) : new GenericSelector(start); + } + + static IOWorkerSelector newSelector(final int workerCount) { + return newSelector(workerCount, 0); + } + + static boolean isPowerOfTwo(final int n) { + return (n & -n) == n; + } + + static final class PowerOfTwoSelector implements IOWorkerSelector { + + private final AtomicInteger idx; + + PowerOfTwoSelector(final int n) { + this.idx = new AtomicInteger(n); + } + + @Override + public int select(final IOWorkerStats[] dispatchers) { + return idx.getAndIncrement() & (dispatchers.length - 1); + } + + } + + static final class GenericSelector implements IOWorkerSelector { + + private final AtomicInteger idx; + + GenericSelector(final int n) { + this.idx = new AtomicInteger(n); + } + + @Override + public int select(final IOWorkerStats[] dispatchers) { + return (idx.getAndIncrement() & Integer.MAX_VALUE) % dispatchers.length; + } + + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerStats.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerStats.java new file mode 100644 index 0000000000..75ec5fea72 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkerStats.java @@ -0,0 +1,50 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.reactor; + +import org.apache.hc.core5.annotation.Internal; + +/** + * Internal I/O dispatch stats that can be used by {@link IOWorkerSelector} + * to select the best suited worker to get new I/O channels. + * + * @since 5.4 + */ +@Internal +public interface IOWorkerStats { + + // Relatively expensive + int totalChannelCount(); + + // Relatively expensive + int pendingChannelCount(); + + // Cheap + long lastSelectMilli(); + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkers.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkers.java deleted file mode 100644 index c7cadac330..0000000000 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOWorkers.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ -package org.apache.hc.core5.reactor; - -import java.util.concurrent.atomic.AtomicInteger; - -final class IOWorkers { - - interface Selector { - - SingleCoreIOReactor next(); - - } - - static Selector newSelector(final SingleCoreIOReactor[] dispatchers) { - return isPowerOfTwo(dispatchers.length) - ? new PowerOfTwoSelector(dispatchers) - : new GenericSelector(dispatchers); - } - - private static boolean isPowerOfTwo(final int val) { - return (val & -val) == val; - } - - private static void validate(final SingleCoreIOReactor dispatcher) { - if (dispatcher.getStatus() == IOReactorStatus.SHUT_DOWN) { - throw new IOReactorShutdownException("I/O reactor has been shut down"); - } - } - - private static final class PowerOfTwoSelector implements Selector { - - private final AtomicInteger idx = new AtomicInteger(0); - private final SingleCoreIOReactor[] dispatchers; - - PowerOfTwoSelector(final SingleCoreIOReactor[] dispatchers) { - this.dispatchers = dispatchers; - } - - @Override - public SingleCoreIOReactor next() { - final SingleCoreIOReactor dispatcher = dispatchers[idx.getAndIncrement() & (dispatchers.length - 1)]; - validate(dispatcher); - return dispatcher; - } - } - - private static final class GenericSelector implements Selector { - - private final AtomicInteger idx = new AtomicInteger(0); - private final SingleCoreIOReactor[] dispatchers; - - GenericSelector(final SingleCoreIOReactor[] dispatchers) { - this.dispatchers = dispatchers; - } - - @Override - public SingleCoreIOReactor next() { - final SingleCoreIOReactor dispatcher = dispatchers[(idx.getAndIncrement() & Integer.MAX_VALUE) % dispatchers.length]; - validate(dispatcher); - return dispatcher; - } - } - -} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalConnectChannel.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalConnectChannel.java index e3dcabb8c3..92c8b2a23a 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalConnectChannel.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalConnectChannel.java @@ -110,8 +110,14 @@ void onException(final Exception cause) { @Override public void close() throws IOException { - key.cancel(); - socketChannel.close(); + try { + if (!sessionRequest.isDone()) { + sessionRequest.cancel(); + } + } finally { + key.cancel(); + socketChannel.close(); + } } @Override diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalDataChannel.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalDataChannel.java index 5d64c00fb4..d290939b4d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalDataChannel.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/InternalDataChannel.java @@ -434,7 +434,7 @@ public void registerProtocol(final String protocolId, final ProtocolUpgradeHandl @Override public String toString() { final IOSession currentSession = currentSessionRef.get(); - return Objects.toString(currentSession != null ? currentSession: ioSession, null); + return Objects.toString(currentSession != null ? currentSession : ioSession, null); } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ProtocolIOSession.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ProtocolIOSession.java index cebfb06d9f..7996a7bd40 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ProtocolIOSession.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ProtocolIOSession.java @@ -40,6 +40,7 @@ public interface ProtocolIOSession extends IOSession, TransportSecurityLayer { /** * Switches this I/O session to the application protocol with the given ID. + * * @param protocolId the application protocol ID * @param callback the result callback * @throws UnsupportedOperationException if application protocol switch @@ -52,10 +53,8 @@ default void switchProtocol(String protocolId, FutureCallback /** * Registers protocol upgrade handler with the given application protocol ID. * - * @since 5.2 * @param protocolId the application protocol ID * @param upgradeHandler the upgrade handler. - * * @since 5.2 */ default void registerProtocol(String protocolId, ProtocolUpgradeHandler upgradeHandler) { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java index 8661d67f29..7510c26f9e 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java @@ -29,9 +29,11 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.net.ProtocolFamily; import java.net.Socket; import java.net.SocketAddress; -import java.net.SocketOption; +import java.net.StandardProtocolFamily; +import java.net.StandardSocketOptions; import java.net.UnknownHostException; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; @@ -42,18 +44,21 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import jdk.net.ExtendedSocketOptions; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.function.Decorator; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.io.Closer; -import org.apache.hc.core5.io.SocketSupport; import org.apache.hc.core5.net.NamedEndpoint; import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.ReflectionUtils; import org.apache.hc.core5.util.Timeout; -class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements ConnectionInitiator { +class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements ConnectionInitiator, IOWorkerStats { private static final int MAX_CHANNEL_REQUESTS = 10000; @@ -68,6 +73,12 @@ class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements Connect private final AtomicBoolean shutdownInitiated; private final long selectTimeoutMillis; private volatile long lastTimeoutCheckMillis; + private volatile long lastSelectMillis; + private final IOReactorMetricsListener threadPoolListener; + + // Atomic variables for tracking total wait time and count of processed requests + private final AtomicLong totalWaitTime = new AtomicLong(0); + private final AtomicInteger processedRequestCount = new AtomicInteger(0); SingleCoreIOReactor( final Callback exceptionCallback, @@ -75,12 +86,14 @@ class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements Connect final IOReactorConfig reactorConfig, final Decorator ioSessionDecorator, final IOSessionListener sessionListener, + final IOReactorMetricsListener threadPoolListener, final Callback sessionShutdownCallback) { super(exceptionCallback); this.eventHandlerFactory = Args.notNull(eventHandlerFactory, "Event handler factory"); this.reactorConfig = Args.notNull(reactorConfig, "I/O reactor config"); this.ioSessionDecorator = ioSessionDecorator; this.sessionListener = sessionListener; + this.threadPoolListener = threadPoolListener; this.sessionShutdownCallback = sessionShutdownCallback; this.shutdownInitiated = new AtomicBoolean(); this.closedSessions = new ConcurrentLinkedQueue<>(); @@ -121,6 +134,7 @@ void doExecute() throws IOException { } // Process selected I/O events + lastSelectMillis = System.currentTimeMillis(); if (readyCount > 0) { processEvents(this.selector.selectedKeys()); } @@ -136,6 +150,8 @@ void doExecute() throws IOException { processPendingConnectionRequests(); } + reportStatusToThreadPoolListener(); + // Exit select loop if graceful shutdown has been completed if (getStatus() == IOReactorStatus.SHUTTING_DOWN && this.selector.keys().isEmpty()) { break; @@ -263,44 +279,40 @@ public Future connect( return sessionRequest; } + @SuppressWarnings("Since15") private void prepareSocket(final SocketChannel socketChannel) throws IOException { - final Socket socket = socketChannel.socket(); - socket.setTcpNoDelay(this.reactorConfig.isTcpNoDelay()); - socket.setKeepAlive(this.reactorConfig.isSoKeepAlive()); if (this.reactorConfig.getSndBufSize() > 0) { - socket.setSendBufferSize(this.reactorConfig.getSndBufSize()); + socketChannel.setOption(StandardSocketOptions.SO_SNDBUF, this.reactorConfig.getSndBufSize()); } if (this.reactorConfig.getRcvBufSize() > 0) { - socket.setReceiveBufferSize(this.reactorConfig.getRcvBufSize()); - } - if (this.reactorConfig.getTrafficClass() > 0) { - socket.setTrafficClass(this.reactorConfig.getTrafficClass()); + socketChannel.setOption(StandardSocketOptions.SO_RCVBUF, this.reactorConfig.getRcvBufSize()); } final int linger = this.reactorConfig.getSoLinger().toSecondsIntBound(); if (linger >= 0) { - socket.setSoLinger(true, linger); - } - if (this.reactorConfig.getTcpKeepIdle() > 0) { - setExtendedSocketOption(socketChannel, SocketSupport.TCP_KEEPIDLE, this.reactorConfig.getTcpKeepIdle()); - } - if (this.reactorConfig.getTcpKeepInterval() > 0) { - setExtendedSocketOption(socketChannel, SocketSupport.TCP_KEEPINTERVAL, this.reactorConfig.getTcpKeepInterval()); + socketChannel.setOption(StandardSocketOptions.SO_LINGER, linger); } - if (this.reactorConfig.getTcpKeepInterval() > 0) { - setExtendedSocketOption(socketChannel, SocketSupport.TCP_KEEPCOUNT, this.reactorConfig.getTcpKeepCount()); + + // None of the below options are applicable to Unix domain sockets. + if (!(socketChannel.getRemoteAddress() instanceof InetSocketAddress)) { + return; } - } + socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, this.reactorConfig.isTcpNoDelay()); + socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, this.reactorConfig.isSoKeepAlive()); - /** - * @since 5.3 - */ - void setExtendedSocketOption(final SocketChannel socketChannel, - final String optionName, final T value) throws IOException { - final SocketOption socketOption = SocketSupport.getExtendedSocketOptionOrNull(optionName); - if (socketOption == null) { - throw new UnsupportedOperationException(optionName + " is not supported in the current jdk"); + if (this.reactorConfig.getTrafficClass() > 0) { + socketChannel.setOption(StandardSocketOptions.IP_TOS, this.reactorConfig.getTrafficClass()); + } + if (ReflectionUtils.supportsKeepAliveOptions()) { + if (this.reactorConfig.getTcpKeepIdle() > 0) { + socketChannel.setOption(ExtendedSocketOptions.TCP_KEEPIDLE, this.reactorConfig.getTcpKeepIdle()); + } + if (this.reactorConfig.getTcpKeepInterval() > 0) { + socketChannel.setOption(ExtendedSocketOptions.TCP_KEEPINTERVAL, this.reactorConfig.getTcpKeepInterval()); + } + if (this.reactorConfig.getTcpKeepCount() > 0) { + socketChannel.setOption(ExtendedSocketOptions.TCP_KEEPCOUNT, this.reactorConfig.getTcpKeepCount()); + } } - socketChannel.setOption(socketOption, value); } private void validateAddress(final SocketAddress address) throws UnknownHostException { @@ -315,10 +327,19 @@ private void validateAddress(final SocketAddress address) throws UnknownHostExce private void processPendingConnectionRequests() { IOSessionRequest sessionRequest; for (int i = 0; i < MAX_CHANNEL_REQUESTS && (sessionRequest = this.requestQueue.poll()) != null; i++) { + if (threadPoolListener != null) { + // Calculate wait time safely without keeping long-lived state + final long waitTimeMillis = System.currentTimeMillis() - sessionRequest.getEnqueueTime(); + + // Accumulate total wait time and increment count atomically + totalWaitTime.addAndGet(waitTimeMillis); + processedRequestCount.incrementAndGet(); + threadPoolListener.onQueueWaitTime(waitTimeMillis); + } if (!sessionRequest.isCancelled()) { final SocketChannel socketChannel; try { - socketChannel = SocketChannel.open(); + socketChannel = openSocketFor(sessionRequest.remoteAddress); } catch (final IOException ex) { sessionRequest.failed(ex); return; @@ -333,6 +354,18 @@ private void processPendingConnectionRequests() { } } + private static SocketChannel openSocketFor(final SocketAddress remoteAddress) throws IOException { + if (remoteAddress instanceof InetSocketAddress) { + return SocketChannel.open(); + } + try { + return (SocketChannel) SocketChannel.class.getMethod("open", ProtocolFamily.class) + .invoke(null, StandardProtocolFamily.valueOf("UNIX")); + } catch (final ReflectiveOperationException e) { + throw new UnsupportedOperationException("UNIX-family socket channels not supported", e); + } + } + private void processConnectionRequest(final SocketChannel socketChannel, final IOSessionRequest sessionRequest) throws IOException { socketChannel.configureBlocking(false); prepareSocket(socketChannel); @@ -394,4 +427,70 @@ private void closePendingConnectionRequests() { } } + /** + * Reports the current status of the I/O reactor's thread pool to the + * configured metrics listener. + * + *

    This method gathers three key metrics:

    + *
      + *
    • Active Threads: The number of currently active threads + * handling I/O sessions.
    • + *
    • Pending Connections: The number of connection requests + * waiting to be processed.
    • + *
    • Saturation Percentage: The ratio of active threads to the + * maximum allowed connections (defined by {@code MAX_CHANNEL_REQUESTS}), + * expressed as a percentage. It provides insight into how saturated the thread + * pool is relative to its maximum capacity. The formula for calculating saturation + * is: + *
      +     *     saturationPercentage = (activeThreads / MAX_CHANNEL_REQUESTS) * 100.0
      +     *     
    • + *
    + *

    + * If the number of pending connections exceeds {@code MAX_CHANNEL_REQUESTS}, + * resource starvation is detected, and an appropriate event is reported. + * + */ + private void reportStatusToThreadPoolListener() { + if (threadPoolListener != null) { + + // Calculate the number of active threads (connections) + final int activeThreads = (int) this.selector.keys().stream() + .filter(key -> key.isValid() && key.attachment() instanceof InternalChannel) + .count(); + + // Calculate the number of pending connection requests + final int pendingConnections = this.requestQueue.size(); + + // Calculate saturation as a percentage of active connections to max allowed connections + final double saturationPercentage = ((double) activeThreads / MAX_CHANNEL_REQUESTS) * 100.0; + + // Report thread pool status: active sessions and pending connections + threadPoolListener.onThreadPoolStatus(activeThreads, pendingConnections); + + // Report thread pool saturation + threadPoolListener.onThreadPoolSaturation(saturationPercentage); + + // Detect resource starvation if pending connections exceed threshold + if (pendingConnections > MAX_CHANNEL_REQUESTS) { + threadPoolListener.onResourceStarvationDetected(); + } + } + } + + @Override + public int totalChannelCount() { + return selector.keys().size(); + } + + @Override + public int pendingChannelCount() { + return channelQueue.size() + requestQueue.size(); + } + + @Override + public long lastSelectMilli() { + return lastSelectMillis; + } + } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/SocksProxyProtocolHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/SocksProxyProtocolHandler.java index e3565be0e3..ddab854bf8 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/SocksProxyProtocolHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/SocksProxyProtocolHandler.java @@ -295,7 +295,7 @@ private void prepareConnectCommand() throws IOException { if (!(sessionRequest.remoteAddress instanceof InetSocketAddress)) { throw new IOException("Unsupported address class: " + sessionRequest.remoteAddress.getClass()); } - final InetSocketAddress targetAddress = ((InetSocketAddress) sessionRequest.remoteAddress); + final InetSocketAddress targetAddress = (InetSocketAddress) sessionRequest.remoteAddress; if (targetAddress.isUnresolved()) { this.buffer.put(ATYP_DOMAINNAME); final String hostName = targetAddress.getHostName(); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java index b48c495431..02298eb5d6 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLIOSession.java @@ -93,6 +93,7 @@ enum TLSHandShakeState { READY, INITIALIZED, HANDSHAKING, COMPLETE } private final AtomicInteger outboundClosedCount; private final AtomicReference handshakeStateRef; private final IOEventHandler internalEventHandler; + private final int packetBufferSize; private int appEventMask; @@ -100,6 +101,7 @@ enum TLSHandShakeState { READY, INITIALIZED, HANDSHAKING, COMPLETE } private volatile Status status = Status.ACTIVE; private volatile Timeout socketTimeout; private volatile TlsDetails tlsDetails; + private volatile boolean appClosed; /** * Creates new instance of {@code SSLIOSession} class. @@ -178,9 +180,9 @@ public SSLIOSession( final SSLSession sslSession = this.sslEngine.getSession(); // Allocate buffers for network (encrypted) data - final int netBufferSize = sslSession.getPacketBufferSize(); - this.inEncrypted = SSLManagedBuffer.create(sslBufferMode, netBufferSize); - this.outEncrypted = SSLManagedBuffer.create(sslBufferMode, netBufferSize); + this.packetBufferSize = sslSession.getPacketBufferSize(); + this.inEncrypted = SSLManagedBuffer.create(sslBufferMode, packetBufferSize); + this.outEncrypted = SSLManagedBuffer.create(sslBufferMode, packetBufferSize); // Allocate buffers for application (unencrypted) data final int appBufferSize = sslSession.getApplicationBufferSize(); @@ -476,7 +478,8 @@ private void updateEventMask() { && (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING || handshakeStatus == HandshakeStatus.FINISHED) && !this.outEncrypted.hasData() && this.sslEngine.isOutboundDone() - && (this.endOfStream || this.sslEngine.isInboundDone())) { + && (this.endOfStream || this.sslEngine.isInboundDone()) + && appClosed) { this.status = Status.CLOSED; } // Abnormal session termination @@ -519,8 +522,6 @@ private void updateEventMask() { // Do we have encrypted data ready to be sent? if (this.outEncrypted.hasData()) { newMask = newMask | EventMask.WRITE; - } else if (this.sslEngine.isOutboundDone()) { - newMask = newMask & ~EventMask.WRITE; } // Update the mask if necessary @@ -611,7 +612,7 @@ private void decryptData(final IOSession protocolSession) throws IOException { if (sslEngine.isInboundDone()) { endOfStream = true; } - if(inPlainBuf.position() > 0) { + if (inPlainBuf.position() > 0) { inPlainBuf.flip(); try { ensureHandler().inputReady(protocolSession, inPlainBuf.hasRemaining() ? inPlainBuf : null); @@ -619,10 +620,11 @@ private void decryptData(final IOSession protocolSession) throws IOException { inPlainBuf.clear(); } } - if (result.getStatus() != SSLEngineResult.Status.OK) { - if (result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW && endOfStream) { - throw new SSLException("Unable to decrypt incoming data due to unexpected end of stream"); - } + if (result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW && endOfStream) { + throw new SSLException("Unable to decrypt incoming data due to unexpected end of stream"); + } + if (result.getStatus() != SSLEngineResult.Status.OK || + result.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING && result.getHandshakeStatus() != HandshakeStatus.FINISHED) { break; } } finally { @@ -647,7 +649,7 @@ private void encryptData(final IOSession protocolSession) throws IOException { this.session.getLock().lock(); try { appReady = (this.appEventMask & SelectionKey.OP_WRITE) > 0 - && this.status == Status.ACTIVE + && this.status.compareTo(Status.CLOSED) < 0 && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING; } finally { this.session.getLock().unlock(); @@ -668,9 +670,18 @@ public int write(final ByteBuffer src) throws IOException { if (this.handshakeStateRef.get() == TLSHandShakeState.READY) { return 0; } - final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire(); - final SSLEngineResult result = doWrap(src, outEncryptedBuf); - return result.bytesConsumed(); + + for (;;) { + final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire(); + final SSLEngineResult result = doWrap(src, outEncryptedBuf); + if (result.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW) { + // We don't release the buffer here, it will be expanded (if needed) + // and returned by the next attempt of SSLManagedBuffer#acquire() call. + this.outEncrypted.ensureWriteable(packetBufferSize); + } else { + return result.bytesConsumed(); + } + } } finally { this.session.getLock().unlock(); } @@ -714,6 +725,7 @@ public void close() { public void close(final CloseMode closeMode) { this.session.getLock().lock(); try { + appClosed = true; if (closeMode == CloseMode.GRACEFUL) { if (this.status.compareTo(Status.CLOSING) >= 0) { return; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLManagedBuffer.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLManagedBuffer.java index e3d21738a6..1a184a7b88 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLManagedBuffer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/ssl/SSLManagedBuffer.java @@ -57,13 +57,54 @@ abstract class SSLManagedBuffer { */ abstract boolean hasData(); + /** + * Expands the underlying buffer's to make sure it has enough write capacity to accommodate + * the required amount of bytes. This method has no side effect if the buffer has enough writeable + * capacity left. + * @param size the required write capacity + */ + abstract void ensureWriteable(final int size); + + /** + * Helper method to ensure additional writeable capacity with respect to the source buffer. It + * allocates a new buffer and copies all the data if needed, returning the new buffer. This method + * has no side effect if the source buffer has enough writeable capacity left. + * @param src source buffer + * @param size the required write capacity + * @return new buffer (or the source buffer of it has enough writeable capacity left) + */ + ByteBuffer ensureWriteable(final ByteBuffer src, final int size) { + if (src == null) { + // Nothing to do, the buffer is not allocated + return null; + } + + // There is not enough capacity left, we need to expand + if (src.remaining() < size) { + final int additionalCapacityNeeded = size - src.remaining(); + final ByteBuffer expanded = ByteBuffer.allocate(src.capacity() + additionalCapacityNeeded); + + // use a duplicated buffer so we don't disrupt the limit of the original buffer + final ByteBuffer tmp = src.duplicate(); + tmp.flip(); + + // Copy to expanded buffer + expanded.put(tmp); + + // Use a new buffer + return expanded; + } else { + return src; + } + } + static SSLManagedBuffer create(final SSLBufferMode mode, final int size) { return mode == SSLBufferMode.DYNAMIC ? new DynamicBuffer(size) : new StaticBuffer(size); } static final class StaticBuffer extends SSLManagedBuffer { - private final ByteBuffer buffer; + private ByteBuffer buffer; public StaticBuffer(final int size) { Args.positive(size, "size"); @@ -90,6 +131,10 @@ public boolean hasData() { return buffer.position() > 0; } + @Override + void ensureWriteable(final int size) { + buffer = ensureWriteable(buffer, size); + } } static final class DynamicBuffer extends SSLManagedBuffer { @@ -126,6 +171,10 @@ public boolean hasData() { return wrapped != null && wrapped.position() > 0; } + @Override + void ensureWriteable(final int size) { + wrapped = ensureWriteable(wrapped, size); + } } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java index ec7e7377a3..7bd4a95d5b 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/ssl/SSLContextBuilder.java @@ -81,7 +81,7 @@ */ public class SSLContextBuilder { - static final String TLS = "TLS"; + static final String TLS = "TLS"; private String protocol; private final Set keyManagers; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java b/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java index 819ad7d3f4..fb660cff3a 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java @@ -76,9 +76,9 @@ public void append(final byte[] b, final int off, final int len) { if (b == null) { return; } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length); + if (off < 0 || off > b.length || len < 0 || + (off + len) < 0 || (off + len) > b.length) { + throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length); } if (len == 0) { return; @@ -124,9 +124,9 @@ public void append(final char[] b, final int off, final int len) { if (b == null) { return; } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length); + if (off < 0 || off > b.length || len < 0 || + (off + len) < 0 || (off + len) > b.length) { + throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length); } if (len == 0) { return; @@ -275,7 +275,7 @@ public byte[] array() { */ public void setLength(final int len) { if (len < 0 || len > this.array.length) { - throw new IndexOutOfBoundsException("len: "+len+" < 0 or > buffer len: "+this.array.length); + throw new IndexOutOfBoundsException("len: " + len + " < 0 or > buffer len: " + this.array.length); } this.len = len; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/CharArrayBuffer.java b/httpcore5/src/main/java/org/apache/hc/core5/util/CharArrayBuffer.java index 40d2d5acff..84668fcd43 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/CharArrayBuffer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/CharArrayBuffer.java @@ -78,9 +78,9 @@ public void append(final char[] b, final int off, final int len) { if (b == null) { return; } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length); + if (off < 0 || off > b.length || len < 0 || + (off + len) < 0 || (off + len) > b.length) { + throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length); } if (len == 0) { return; @@ -177,9 +177,9 @@ public void append(final byte[] b, final int off, final int len) { if (b == null) { return; } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length); + if (off < 0 || off > b.length || len < 0 || + (off + len) < 0 || (off + len) > b.length) { + throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length); } if (len == 0) { return; @@ -322,7 +322,7 @@ public void ensureCapacity(final int required) { */ public void setLength(final int len) { if (len < 0 || len > this.array.length) { - throw new IndexOutOfBoundsException("len: "+len+" < 0 or > buffer len: "+this.array.length); + throw new IndexOutOfBoundsException("len: " + len + " < 0 or > buffer len: " + this.array.length); } this.len = len; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/Deadline.java b/httpcore5/src/main/java/org/apache/hc/core5/util/Deadline.java index efbaaed950..29c493f061 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/Deadline.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/Deadline.java @@ -83,9 +83,8 @@ public class Deadline { */ public static Deadline calculate(final long timeMillis, final TimeValue timeValue) { if (TimeValue.isPositive(timeValue)) { - // TODO handle unlikely overflow - final long deadline = timeMillis + timeValue.toMilliseconds(); - return deadline < 0 ? Deadline.MAX_VALUE : Deadline.fromUnixMilliseconds(deadline); + return Deadline.fromUnixMilliseconds(timeMillis + + Math.min(timeValue.toMilliseconds(), Long.MAX_VALUE - timeMillis)); } return Deadline.MAX_VALUE; } @@ -302,7 +301,8 @@ public TimeValue remainingTimeValue() { private void setLastCheck() { if (!frozen) { this.lastCheck = System.currentTimeMillis(); - }} + } + } @Override public String toString() { diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java index 749268dbae..4ef59075e1 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java @@ -27,12 +27,20 @@ package org.apache.hc.core5.util; -import java.lang.reflect.Method; - +import jdk.net.ExtendedSocketOptions; +import jdk.net.Sockets; import org.apache.hc.core5.annotation.Internal; +import java.lang.reflect.Method; +import java.net.Socket; +import java.util.Arrays; + @Internal +@SuppressWarnings("Since15") public final class ReflectionUtils { + private static final boolean SUPPORTS_KEEPALIVE_OPTIONS = Sockets.supportedOptions(Socket.class) + .containsAll(Arrays.asList(ExtendedSocketOptions.TCP_KEEPIDLE, ExtendedSocketOptions.TCP_KEEPINTERVAL, + ExtendedSocketOptions.TCP_KEEPCOUNT)); public static void callSetter(final Object object, final String setterName, final Class type, final Object value) { try { @@ -89,4 +97,7 @@ public static int determineJRELevel() { return 7; } + public static boolean supportsKeepAliveOptions() { + return SUPPORTS_KEEPALIVE_OPTIONS; + } } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/TextUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/util/TextUtils.java index 73864f0a05..dffeaf58f3 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/TextUtils.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/TextUtils.java @@ -124,17 +124,16 @@ public static boolean containsBlanks(final CharSequence s) { * * @since 5.0 */ + private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', + 'e', 'f' }; + public static String toHexString(final byte[] bytes) { if (bytes == null) { return null; } final StringBuilder buffer = new StringBuilder(bytes.length * 2); - for (final byte element : bytes) { - final int unsignedB = element & 0xff; - if (unsignedB < 16) { - buffer.append('0'); - } - buffer.append(Integer.toHexString(unsignedB)); + for (final byte b : bytes) { + buffer.append(HEX_CHARS[(0xf0 & b) >>> 4]).append(HEX_CHARS[0x0f & b]); } return buffer.toString(); } @@ -181,9 +180,9 @@ public static boolean isAllASCII(final CharSequence s) { */ @Internal public static byte castAsByte(final int c) { - if ((c >= 0x20 && c <= 0x7E) || // Visible ASCII - (c >= 0xA0 && c <= 0xFF) || // Visible ISO-8859-1 - c == 0x09) { // TAB + if (c >= 0x20 && c <= 0x7E || // Visible ASCII + c >= 0xA0 && c <= 0xFF || // Visible ISO-8859-1 + c == 0x09) { // TAB return (byte) c; } return '?'; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/Tokenizer.java b/httpcore5/src/main/java/org/apache/hc/core5/util/Tokenizer.java index 5807eb9f92..1362394f6b 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/Tokenizer.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/Tokenizer.java @@ -334,7 +334,7 @@ public void copyContent(final CharSequence buf, final Cursor cursor, final Delim final int indexTo = cursor.getUpperBound(); for (int i = indexFrom; i < indexTo; i++) { final char current = buf.charAt(i); - if ((delimiterPredicate != null && delimiterPredicate.test(current)) || isWhitespace(current)) { + if (delimiterPredicate != null && delimiterPredicate.test(current) || isWhitespace(current)) { break; } pos++; @@ -372,7 +372,7 @@ public void copyUnquotedContent(final CharSequence buf, final Cursor cursor, final int indexTo = cursor.getUpperBound(); for (int i = indexFrom; i < indexTo; i++) { final char current = buf.charAt(i); - if ((delimiterPredicate != null && delimiterPredicate.test(current)) + if (delimiterPredicate != null && delimiterPredicate.test(current) || isWhitespace(current) || current == DQUOTE) { break; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/VersionInfo.java b/httpcore5/src/main/java/org/apache/hc/core5/util/VersionInfo.java index edcb6b5663..947fa55d4d 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/util/VersionInfo.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/util/VersionInfo.java @@ -56,8 +56,8 @@ public class VersionInfo { public final static String VERSION_PROPERTY_FILE = "version.properties"; // the property names - public final static String PROPERTY_MODULE = "info.module"; - public final static String PROPERTY_RELEASE = "info.release"; + public final static String PROPERTY_MODULE = "info.module"; + public final static String PROPERTY_RELEASE = "info.release"; /** * @deprecated This will be removed in 6.0. */ @@ -98,11 +98,11 @@ public class VersionInfo { protected VersionInfo(final String pckg, final String module, final String release, final String time, final String clsldr) { Args.notNull(pckg, "Package identifier"); - infoPackage = pckg; - infoModule = (module != null) ? module : UNAVAILABLE; - infoRelease = (release != null) ? release : UNAVAILABLE; - infoTimestamp = (time != null) ? time : UNAVAILABLE; - infoClassloader = (clsldr != null) ? clsldr : UNAVAILABLE; + infoPackage = pckg; + infoModule = module != null ? module : UNAVAILABLE; + infoRelease = release != null ? release : UNAVAILABLE; + infoTimestamp = time != null ? time : UNAVAILABLE; + infoClassloader = clsldr != null ? clsldr : UNAVAILABLE; } @@ -274,13 +274,12 @@ protected static VersionInfo fromMap(final String pckg, final Map info, if (info != null) { module = (String) info.get(PROPERTY_MODULE); - if ((module != null) && (module.length() < 1)) { + if (module != null && module.length() < 1) { module = null; } release = (String) info.get(PROPERTY_RELEASE); - if ((release != null) && ((release.length() < 1) || - (release.equals("${project.version}")))) { + if (release != null && (release.length() < 1 || release.equals("${project.version}"))) { release = null; } } // if info diff --git a/httpcore5/src/test/java/org/apache/hc/core5/annotation/ThreadingBehaviorTest.java b/httpcore5/src/test/java/org/apache/hc/core5/annotation/ThreadingBehaviorTest.java index c2ab2154b0..1031fd0cbc 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/annotation/ThreadingBehaviorTest.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/annotation/ThreadingBehaviorTest.java @@ -34,7 +34,7 @@ class ThreadingBehaviorTest { @Test - void testName(){ + void testName() { assertEquals("SAFE", ThreadingBehavior.SAFE.name()); assertEquals("SAFE_CONDITIONAL", ThreadingBehavior.SAFE_CONDITIONAL.name()); assertEquals("IMMUTABLE_CONDITIONAL", ThreadingBehavior.IMMUTABLE_CONDITIONAL.name()); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/concurrent/TestBasicFuture.java b/httpcore5/src/test/java/org/apache/hc/core5/concurrent/TestBasicFuture.java index 432ea16da2..8be62908cb 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/concurrent/TestBasicFuture.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/concurrent/TestBasicFuture.java @@ -138,7 +138,7 @@ void testAsyncCompleted() throws Exception { try { Thread.sleep(100); future.completed(result); - } catch (final InterruptedException boom) { + } catch (final InterruptedException expected) { } }); t.setDaemon(true); @@ -157,7 +157,7 @@ void testAsyncFailed() throws Exception { try { Thread.sleep(100); future.failed(boom); - } catch (final InterruptedException ex) { + } catch (final InterruptedException expected) { } }); t.setDaemon(true); @@ -179,7 +179,7 @@ void testAsyncCancelled() { try { Thread.sleep(100); future.cancel(true); - } catch (final InterruptedException ex) { + } catch (final InterruptedException expected) { } }); t.setDaemon(true); @@ -197,7 +197,7 @@ void testAsyncTimeout() { try { Thread.sleep(200); future.completed(result); - } catch (final InterruptedException ex) { + } catch (final InterruptedException expected) { } }); t.setDaemon(true); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/TestContentType.java b/httpcore5/src/test/java/org/apache/hc/core5/http/TestContentType.java index cea1eae592..c0e5c00f80 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/TestContentType.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/TestContentType.java @@ -40,6 +40,14 @@ */ class TestContentType { + @Test + void testApplicationZipCompressed() { + final ContentType contentType = ContentType.create("application/x-zip-compressed"); + Assertions.assertEquals(contentType.toString(), ContentType.APPLICATION_ZIP_COMPRESSED.toString()); + Assertions.assertTrue(contentType.isSameMimeType(ContentType.APPLICATION_ZIP_COMPRESSED)); + Assertions.assertTrue(ContentType.APPLICATION_ZIP_COMPRESSED.isSameMimeType(contentType)); + } + @Test void testBasis() throws Exception { final ContentType contentType = ContentType.create("text/plain", "US-ASCII"); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/TestHttpVersion.java b/httpcore5/src/test/java/org/apache/hc/core5/http/TestHttpVersion.java index 70f17a2673..c3e078fbe3 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/TestHttpVersion.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/TestHttpVersion.java @@ -88,15 +88,15 @@ void testHttpVersionEquality() { Assertions.assertNotEquals(ver1, Float.valueOf(1.1f)); - Assertions.assertEquals(HttpVersion.HTTP_0_9, (new HttpVersion(0, 9))); - Assertions.assertEquals(HttpVersion.HTTP_1_0, (new HttpVersion(1, 0))); - Assertions.assertEquals(HttpVersion.HTTP_1_1, (new HttpVersion(1, 1))); - Assertions.assertNotEquals(HttpVersion.HTTP_1_0, (new HttpVersion(1, 1))); - - Assertions.assertEquals(HttpVersion.HTTP_0_9, (new ProtocolVersion("HTTP", 0, 9))); - Assertions.assertEquals(HttpVersion.HTTP_1_0, (new ProtocolVersion("HTTP", 1, 0))); - Assertions.assertEquals(HttpVersion.HTTP_1_1, (new ProtocolVersion("HTTP", 1, 1))); - Assertions.assertNotEquals(HttpVersion.HTTP_1_1, (new ProtocolVersion("http", 1, 1))); + Assertions.assertEquals(HttpVersion.HTTP_0_9, new HttpVersion(0, 9)); + Assertions.assertEquals(HttpVersion.HTTP_1_0, new HttpVersion(1, 0)); + Assertions.assertEquals(HttpVersion.HTTP_1_1, new HttpVersion(1, 1)); + Assertions.assertNotEquals(HttpVersion.HTTP_1_0, new HttpVersion(1, 1)); + + Assertions.assertEquals(HttpVersion.HTTP_0_9, new ProtocolVersion("HTTP", 0, 9)); + Assertions.assertEquals(HttpVersion.HTTP_1_0, new ProtocolVersion("HTTP", 1, 0)); + Assertions.assertEquals(HttpVersion.HTTP_1_1, new ProtocolVersion("HTTP", 1, 1)); + Assertions.assertNotEquals(HttpVersion.HTTP_1_1, new ProtocolVersion("http", 1, 1)); Assertions.assertEquals(HttpVersion.HTTP_0_9, new ProtocolVersion("HTTP", 0, 9)); Assertions.assertEquals(HttpVersion.HTTP_1_0, new ProtocolVersion("HTTP", 1, 0)); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicFileServerExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicFileServerExample.java index 63e6ecd677..9b4cea8ec3 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicFileServerExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicFileServerExample.java @@ -131,7 +131,7 @@ public void onError(final HttpConnection conn, final Exception ex) { } - static class HttpFileHandler implements HttpRequestHandler { + static class HttpFileHandler implements HttpRequestHandler { private final String docRoot; diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicReverseProxyExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicReverseProxyExample.java index b2fc53502f..620643e3f7 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicReverseProxyExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/ClassicReverseProxyExample.java @@ -87,13 +87,13 @@ public static void main(final String[] args) throws Exception { @Override public void onRequestHead(final HttpConnection connection, final HttpRequest request) { - System.out.println("[proxy->origin] " + Thread.currentThread() + " " + + System.out.println("[proxy->origin] " + Thread.currentThread() + " " + request.getMethod() + " " + request.getRequestUri()); } @Override public void onResponseHead(final HttpConnection connection, final HttpResponse response) { - System.out.println("[proxy<-origin] " + Thread.currentThread() + " status " + response.getCode()); + System.out.println("[proxy<-origin] " + Thread.currentThread() + " status " + response.getCode()); } @Override @@ -155,7 +155,7 @@ public void onError(final Exception ex) { if (ex instanceof SocketException) { System.out.println("[client->proxy] " + Thread.currentThread() + " " + ex.getMessage()); } else { - System.out.println("[client->proxy] " + Thread.currentThread() + " " + ex.getMessage()); + System.out.println("[client->proxy] " + Thread.currentThread() + " " + ex.getMessage()); ex.printStackTrace(System.out); } } @@ -198,7 +198,7 @@ public void onError(final HttpConnection connection, final Exception ex) { TextUtils.toLowerCase(HttpHeaders.UPGRADE)))); - static class ProxyHandler implements HttpRequestHandler { + static class ProxyHandler implements HttpRequestHandler { private final HttpHost targetHost; private final HttpRequester requester; diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/PrintVersionInfo.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/PrintVersionInfo.java index 85f1a6e1e8..47cbffb902 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/PrintVersionInfo.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/PrintVersionInfo.java @@ -53,10 +53,10 @@ public class PrintVersionInfo { * a list of packages for which to get version info. */ public static void main(final String args[]) { - final String[] pckgs = (args.length > 0) ? args : MODULE_LIST; + final String[] pckgs = (args.length > 0) ? args : MODULE_LIST; VersionInfo[] via = VersionInfo.loadVersionInfo(pckgs, null); System.out.println("version info for thread context classloader:"); - for (int i=0; i 0); } } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestBHttpConnectionBase.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestBHttpConnectionBase.java index 6379695878..e24477dfdf 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestBHttpConnectionBase.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestBHttpConnectionBase.java @@ -159,7 +159,7 @@ void testCreateEntityLengthDelimited() throws Exception { Assertions.assertEquals("chunked", entity.getContentEncoding()); final InputStream content = entity.getContent(); Assertions.assertNotNull(content); - Assertions.assertTrue((content instanceof ContentLengthInputStream)); + Assertions.assertInstanceOf(ContentLengthInputStream.class, content); } @Test @@ -172,7 +172,7 @@ void testCreateEntityInputChunked() throws Exception { Assertions.assertEquals(-1, entity.getContentLength()); final InputStream content = entity.getContent(); Assertions.assertNotNull(content); - Assertions.assertTrue((content instanceof ChunkedInputStream)); + Assertions.assertInstanceOf(ChunkedInputStream.class, content); } @Test @@ -185,7 +185,7 @@ void testCreateEntityInputUndefined() throws Exception { Assertions.assertEquals(-1, entity.getContentLength()); final InputStream content = entity.getContent(); Assertions.assertNotNull(content); - Assertions.assertTrue((content instanceof IdentityInputStream)); + Assertions.assertInstanceOf(IdentityInputStream.class, content); } @Test diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestHttpRequestExecutor.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestHttpRequestExecutor.java index d961c5a283..f9451bf828 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestHttpRequestExecutor.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestHttpRequestExecutor.java @@ -27,9 +27,6 @@ package org.apache.hc.core5.http.impl.io; -import java.io.IOException; -import java.util.List; - import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HeaderElements; @@ -37,12 +34,14 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.io.HttpClientConnection; import org.apache.hc.core5.http.io.HttpResponseInformationCallback; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.util.Timeout; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -50,6 +49,9 @@ import org.mockito.ArgumentMatchers; import org.mockito.Mockito; +import java.io.IOException; +import java.util.List; + class TestHttpRequestExecutor { @Test @@ -402,7 +404,7 @@ void testExecutionIOException() throws Exception { Mockito.doThrow(new IOException("Oopsie")).when(conn).sendRequestHeader(request); Assertions.assertThrows(IOException.class, () -> executor.execute(request, conn, context)); - Mockito.verify(conn).close(); + Mockito.verify(conn).close(CloseMode.IMMEDIATE); } @Test @@ -415,6 +417,19 @@ void testExecutionRuntimeException() throws Exception { Mockito.doThrow(new RuntimeException("Oopsie")).when(conn).receiveResponseHeader(); Assertions.assertThrows(RuntimeException.class, () -> executor.execute(request, conn, context)); + Mockito.verify(conn).close(CloseMode.IMMEDIATE); + } + + @Test + void testExecutionHttpException() throws Exception { + final HttpClientConnection conn = Mockito.mock(HttpClientConnection.class); + final HttpRequestExecutor executor = new HttpRequestExecutor(); + + final HttpCoreContext context = HttpCoreContext.create(); + final ClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + + Mockito.doThrow(new HttpException("Oopsie")).when(conn).receiveResponseHeader(); + Assertions.assertThrows(HttpException.class, () -> executor.execute(request, conn, context)); Mockito.verify(conn).close(); } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestSessionInOutBuffers.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestSessionInOutBuffers.java index 5494306066..22444d811f 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestSessionInOutBuffers.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestSessionInOutBuffers.java @@ -103,7 +103,7 @@ void testBasicReadWriteLine() throws Exception { final long bytesWritten = tmetrics.getBytesTransferred(); long expected = 0; for (final String teststr : teststrs) { - expected += (teststr.length() + 2/*CRLF*/); + expected += teststr.length() + 2/*CRLF*/; } Assertions.assertEquals(expected, bytesWritten); @@ -151,7 +151,7 @@ void testComplexReadWriteLine() throws Exception { outbuffer.write(buffer.toString().getBytes(StandardCharsets.US_ASCII), outputStream); outbuffer.flush(outputStream); bytesWritten = outbuffer.getMetrics().getBytesTransferred(); - Assertions.assertEquals(8 + 14 +2, bytesWritten); + Assertions.assertEquals(8 + 14 + 2, bytesWritten); buffer.setLength(0); for (int i = 0; i < 15; i++) { @@ -249,7 +249,7 @@ void testBasicReadWriteLineLargeBuffer() throws Exception { final long bytesWritten = outbuffer.getMetrics().getBytesTransferred(); long expected = 0; for (final String teststr : teststrs) { - expected += (teststr.length() + 2/*CRLF*/); + expected += teststr.length() + 2/*CRLF*/; } Assertions.assertEquals(expected, bytesWritten); @@ -459,11 +459,11 @@ void testReadLineFringeCase1() throws Exception { Assertions.assertEquals(0, inBuffer1.readLine(chbuffer, inputStream)); } - static final int SWISS_GERMAN_HELLO [] = { + static final int[] SWISS_GERMAN_HELLO = { 0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4 }; - static final int RUSSIAN_HELLO [] = { + static final int[] RUSSIAN_HELLO = { 0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438, 0x432, 0x435, 0x442 }; @@ -501,9 +501,9 @@ void testMultibyteCodedReadWriteLine() throws Exception { } outbuffer.flush(outputStream); final long bytesWritten = outbuffer.getMetrics().getBytesTransferred(); - final long expected = ((s1.getBytes(StandardCharsets.UTF_8).length + 2)+ + final long expected = ((s1.getBytes(StandardCharsets.UTF_8).length + 2) + (s2.getBytes(StandardCharsets.UTF_8).length + 2) + - (s3.getBytes(StandardCharsets.UTF_8).length + 2)) * 10; + (s3.getBytes(StandardCharsets.UTF_8).length + 2)) * 10L; Assertions.assertEquals(expected, bytesWritten); final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16, StandardCharsets.UTF_8.newDecoder()); @@ -572,7 +572,7 @@ void testNonAsciiReadWriteLine() throws Exception { outbuffer.writeLine(chbuffer, outputStream); outbuffer.flush(outputStream); final long bytesWritten = outbuffer.getMetrics().getBytesTransferred(); - final long expected = ((s1.getBytes(StandardCharsets.UTF_8).length + 2)) * 10 + 2; + final long expected = ((s1.getBytes(StandardCharsets.UTF_8).length + 2)) * 10L + 2; Assertions.assertEquals(expected, bytesWritten); final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16, StandardCharsets.UTF_8.newDecoder()); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TimeoutByteArrayInputStream.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TimeoutByteArrayInputStream.java index b27632d40c..a9db7516f9 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TimeoutByteArrayInputStream.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TimeoutByteArrayInputStream.java @@ -69,9 +69,9 @@ public int read() throws IOException { public int read(final byte b[], final int off, final int len) throws IOException { if (b == null) { throw new NullPointerException(); - } else if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) > b.length) || ((off + len) < 0)) { - throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length); + } else if (off < 0 || off > b.length || len < 0 || + (off + len) > b.length || (off + len) < 0) { + throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length); } if (len == 0) { return 0; diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestAbstractHttp1StreamDuplexerCapacityWindow.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestAbstractHttp1StreamDuplexerCapacityWindow.java index 2f85e4434d..211633bb3a 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestAbstractHttp1StreamDuplexerCapacityWindow.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestAbstractHttp1StreamDuplexerCapacityWindow.java @@ -32,7 +32,6 @@ import org.apache.hc.core5.http.impl.nio.AbstractHttp1StreamDuplexer.CapacityWindow; import org.apache.hc.core5.reactor.IOSession; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -93,7 +92,7 @@ void windowCannotUnderflow() { } @Test - void windowCannotOverflow() throws IOException{ + void windowCannotOverflow() throws IOException { final CapacityWindow window = new CapacityWindow(Integer.MAX_VALUE, ioSession); window.update(1); Assertions.assertEquals(Integer.MAX_VALUE, window.getWindow()); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestIdentityDecoder.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestIdentityDecoder.java index f007a72491..5051b68e43 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestIdentityDecoder.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestIdentityDecoder.java @@ -229,7 +229,7 @@ void testDecodingFileWithOffsetAndBufferedSessionData() throws Exception { final FileChannel fchannel = testfile.getChannel(); long pos = beginning.length; while (!decoder.isCompleted()) { - if(testfile.length() < pos) { + if (testfile.length() < pos) { testfile.setLength(pos); } final long bytesRead = decoder.transfer(fchannel, pos, 10); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestLengthDelimitedDecoder.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestLengthDelimitedDecoder.java index 4713672ea5..c97deb57bc 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestLengthDelimitedDecoder.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/nio/TestLengthDelimitedDecoder.java @@ -322,7 +322,7 @@ void testDecodingFileWithOffsetAndBufferedSessionData() throws Exception { final int i = inbuf.fill(channel); Assertions.assertEquals(7, i); - final byte[] beginning = "beginning; ".getBytes(StandardCharsets.US_ASCII); + final byte[] beginning = "beginning; ".getBytes(StandardCharsets.US_ASCII); createTempFile(); RandomAccessFile testfile = new RandomAccessFile(this.tmpfile, "rw"); @@ -338,7 +338,7 @@ void testDecodingFileWithOffsetAndBufferedSessionData() throws Exception { long pos = beginning.length; while (!decoder.isCompleted()) { - if(testfile.length() < pos) { + if (testfile.length() < pos) { testfile.setLength(pos); } final long bytesRead = decoder.transfer(fchannel, pos, 10); @@ -451,7 +451,7 @@ void testCodingBeyondContentLimitFile() throws Exception { channel, inbuf, metrics, 16); createTempFile(); - try (RandomAccessFile testfile = new RandomAccessFile(this.tmpfile, "rw")) { + try (RandomAccessFile testfile = new RandomAccessFile(this.tmpfile, "rw")) { final FileChannel fchannel = testfile.getChannel(); long bytesRead = decoder.transfer(fchannel, 0, 6); @@ -545,7 +545,7 @@ void testTruncatedContentWithFile() throws Exception { channel, inbuf, metrics, 20); createTempFile(); - try (RandomAccessFile testfile = new RandomAccessFile(this.tmpfile, "rw")) { + try (RandomAccessFile testfile = new RandomAccessFile(this.tmpfile, "rw")) { final FileChannel fchannel = testfile.getChannel(); final long bytesRead = decoder.transfer(fchannel, 0, Integer.MAX_VALUE); Assertions.assertEquals(10, bytesRead); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestPathPatternMatcher.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestPathPatternMatcher.java index e2d36876f2..0401ca969f 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestPathPatternMatcher.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestPathPatternMatcher.java @@ -43,6 +43,9 @@ void testPathMatching() { Assertions.assertFalse(matcher.match("/foo/request", "/foo/requesta")); Assertions.assertFalse(matcher.match("/foo/*", "foo/request")); Assertions.assertFalse(matcher.match("/foo/*", "/bar/foo")); + Assertions.assertTrue(matcher.match("*/action.do", "/xxxxx/action.do")); + Assertions.assertTrue(matcher.match("*/foo/action.do", "/xxxxx/foo/action.do")); + Assertions.assertFalse(matcher.match("*.do", "foo.dont")); } @Test @@ -53,6 +56,7 @@ void testBetterMatch() { Assertions.assertTrue(matcher.isBetter("/a*", "*")); Assertions.assertTrue(matcher.isBetter("/*", "*")); Assertions.assertTrue(matcher.isBetter("/a/b*", "/a*")); + Assertions.assertTrue(matcher.isBetter("/a/*", "/a/b")); } } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestRequestRouter.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestRequestRouter.java index 3a9171a092..f3d3a5ca36 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestRequestRouter.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestRequestRouter.java @@ -117,7 +117,7 @@ void testDownstreamResolution() throws Exception { .addRoute(new URIAuthority("somehost", 80), "/*", 1L) .addRoute(new URIAuthority("someotherhost", 80), "/*", 10L) .resolveAuthority((scheme, authority) -> authority) - .downstream(((request, context) -> -1L)) + .downstream((request, context) -> -1L) .build(); final HttpCoreContext context = HttpCoreContext.create(); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestUriPathRouter.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestUriPathRouter.java index c72574db1e..e7bff9a549 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestUriPathRouter.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/routing/TestUriPathRouter.java @@ -38,7 +38,7 @@ class TestUriPathRouter { @Test void testBestMatchWildCardMatching() { - final UriPathRouter.BestMatcher matcher = new UriPathRouter.BestMatcher<>(); + final UriPathRouter.BestMatcher matcher = UriPathRouter.BestMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("*", 0L), new PathRoute<>("/one/*", 1L), @@ -58,7 +58,7 @@ void testBestMatchWildCardMatching() { @Test void testBestMatchWildCardMatchingSuffixPrefixPrecedence() { - final UriPathRouter.BestMatcher matcher = new UriPathRouter.BestMatcher<>(); + final UriPathRouter.BestMatcher matcher = UriPathRouter.BestMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("/ma*", 1L), new PathRoute<>("*tch", 2L)); @@ -67,7 +67,7 @@ void testBestMatchWildCardMatchingSuffixPrefixPrecedence() { @Test void testBestMatchWildCardMatchingExactMatch() { - final UriPathRouter.BestMatcher matcher = new UriPathRouter.BestMatcher<>(); + final UriPathRouter.BestMatcher matcher = UriPathRouter.BestMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("exact", 1L), new PathRoute<>("*", 0L)); @@ -76,7 +76,7 @@ void testBestMatchWildCardMatchingExactMatch() { @Test void testBestMatchWildCardMatchingNoMatch() { - final UriPathRouter.BestMatcher matcher = new UriPathRouter.BestMatcher<>(); + final UriPathRouter.BestMatcher matcher = UriPathRouter.BestMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("/this/*", 1L), new PathRoute<>("/that/*", 2L)); @@ -85,7 +85,7 @@ void testBestMatchWildCardMatchingNoMatch() { @Test void testOrderedWildCardMatching1() { - final UriPathRouter.OrderedMatcher matcher = new UriPathRouter.OrderedMatcher<>(); + final UriPathRouter.OrderedMatcher matcher = UriPathRouter.OrderedMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("*", 0L), new PathRoute<>("/one/*", 1L), @@ -121,7 +121,7 @@ void testOrderedWildCardMatching1() { @Test void testOrderedWildCardMatchingSuffixPrefixPrecedence() { - final UriPathRouter.OrderedMatcher matcher = new UriPathRouter.OrderedMatcher<>(); + final UriPathRouter.OrderedMatcher matcher = UriPathRouter.OrderedMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("/ma*", 1L), new PathRoute<>("*tch", 2L)); @@ -130,7 +130,7 @@ void testOrderedWildCardMatchingSuffixPrefixPrecedence() { @Test void testOrderedStarAndExact() { - final UriPathRouter.OrderedMatcher matcher = new UriPathRouter.OrderedMatcher<>(); + final UriPathRouter.OrderedMatcher matcher = UriPathRouter.OrderedMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("*", 0L), new PathRoute<>("exact", 1L)); @@ -139,7 +139,7 @@ void testOrderedStarAndExact() { @Test void testOrderedExactAndStar() { - final UriPathRouter.OrderedMatcher matcher = new UriPathRouter.OrderedMatcher<>(); + final UriPathRouter.OrderedMatcher matcher = UriPathRouter.OrderedMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>("exact", 1L), new PathRoute<>("*", 0L)); @@ -148,7 +148,7 @@ void testOrderedExactAndStar() { @Test void testRegExMatching() { - final UriPathRouter.RegexMatcher matcher = new UriPathRouter.RegexMatcher<>(); + final UriPathRouter.RegexMatcher matcher = UriPathRouter.RegexMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>(Pattern.compile("/one/two/three/.*"), 3L), new PathRoute<>(Pattern.compile("/one/two/.*"), 2L), @@ -167,7 +167,7 @@ void testRegExMatching() { @Test void testRegExWildCardMatchingSuffixPrefixPrecedence() { - final UriPathRouter.RegexMatcher matcher = new UriPathRouter.RegexMatcher<>(); + final UriPathRouter.RegexMatcher matcher = UriPathRouter.RegexMatcher.getInstance(); final List> routes = Arrays.asList( new PathRoute<>(Pattern.compile("/ma.*"), 1L), new PathRoute<>(Pattern.compile(".*tch"), 2L)); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java index b306d6ac68..3e566895bf 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java @@ -223,7 +223,7 @@ void testParseEntity() throws Exception { void testParseUTF8Entity() throws Exception { final String ru_hello = constructString(RUSSIAN_HELLO); final String ch_hello = constructString(SWISS_GERMAN_HELLO); - final List parameters = new ArrayList<>(); + final List parameters = new ArrayList<>(); parameters.add(new BasicNameValuePair("russian", ru_hello)); parameters.add(new BasicNameValuePair("swiss", ch_hello)); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java index 6701575965..b9a973230a 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java @@ -125,5 +125,6 @@ void testWriteToUnknownLength() throws Exception { void testWriteToNull() throws Exception { try (final InputStreamEntity entity = new InputStreamEntity(EmptyInputStream.INSTANCE, 0, null)) { Assertions.assertThrows(NullPointerException.class, () -> entity.writeTo(null)); - }} + } + } } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestSerializableEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestSerializableEntity.java index c9350bf148..5e73fc02a6 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestSerializableEntity.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestSerializableEntity.java @@ -46,7 +46,9 @@ public static class SerializableObject implements Serializable { public final String stringValue = "Hello"; - public SerializableObject() {} + public SerializableObject() { + } + } @Test diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java index 729c6b89f7..685c42bdf8 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/support/ClassicRequestBuilderTest.java @@ -109,6 +109,19 @@ void head() throws UnknownHostException, URISyntaxException { assertEquals("/localhost", classicRequestBuilder3.getPath()); } + @Test + void query() throws UnknownHostException, URISyntaxException { + final ClassicRequestBuilder classicRequestBuilder = ClassicRequestBuilder.query(); + assertEquals(Method.QUERY.name(), classicRequestBuilder.getMethod()); + + final ClassicRequestBuilder classicRequestBuilder1 = ClassicRequestBuilder.query(URIBuilder.localhost().build()); + assertEquals(Method.QUERY.name(), classicRequestBuilder1.getMethod()); + + final ClassicRequestBuilder classicRequestBuilder3 = ClassicRequestBuilder.query("/localhost"); + assertEquals(Method.QUERY.name(), classicRequestBuilder3.getMethod()); + assertEquals("/localhost", classicRequestBuilder3.getPath()); + } + @Test void patch() throws UnknownHostException, URISyntaxException { final ClassicRequestBuilder classicRequestBuilder = ClassicRequestBuilder.patch(); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/message/HttpRequestWrapperTest.java b/httpcore5/src/test/java/org/apache/hc/core5/http/message/HttpRequestWrapperTest.java index a47a7b7535..d169f4ee4a 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/message/HttpRequestWrapperTest.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/message/HttpRequestWrapperTest.java @@ -88,7 +88,7 @@ void testRequestWithAbsoluteURI() throws Exception { Assertions.assertEquals(new URIAuthority("host", 9443), httpRequestWrapper.getAuthority()); Assertions.assertEquals("https", httpRequestWrapper.getScheme()); Assertions.assertEquals(new URI("https://host:9443/stuff?param=value"), httpRequestWrapper.getUri()); - httpRequestWrapper.setScheme((URIScheme.HTTP.id)); + httpRequestWrapper.setScheme(URIScheme.HTTP.id); Assertions.assertEquals("http", httpRequestWrapper.getScheme()); } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicHeaderIterator.java b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicHeaderIterator.java index 62cbc8c72f..b0217450dc 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicHeaderIterator.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicHeaderIterator.java @@ -78,10 +78,10 @@ void testAllSame() { @Test void testFirstLastOneNone() { final Header[] headers = new Header[]{ - new BasicHeader("match" , "value0"), - new BasicHeader("mismatch", "value1, value1.1"), - new BasicHeader("single" , "value2=whatever"), - new BasicHeader("match" , "value3;tag=nil"), + new BasicHeader("match", "value0"), + new BasicHeader("mismatch", "value1, value1.1"), + new BasicHeader("single", "value2=whatever"), + new BasicHeader("match", "value3;tag=nil"), }; // without filter diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicTokenIterator.java b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicTokenIterator.java index bda1042439..32ffa96cd1 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicTokenIterator.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicTokenIterator.java @@ -37,17 +37,16 @@ /** * Tests for {@link BasicTokenIterator}. - * */ class TestBasicTokenIterator { @Test void testSingleHeader() { Header[] headers = new Header[]{ - new BasicHeader("Name", "token0,token1, token2 , token3") + new BasicHeader("Name", "token0,token1, token2 , token3") }; Iterator

    hit = new BasicHeaderIterator(headers, null); - Iterator ti = new BasicTokenIterator(hit); + Iterator ti = new BasicTokenIterator(hit); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token0", ti.next()); @@ -61,10 +60,10 @@ void testSingleHeader() { headers = new Header[]{ - new BasicHeader("Name", "token0") + new BasicHeader("Name", "token0") }; hit = new BasicHeaderIterator(headers, null); - ti = new BasicTokenIterator(hit); + ti = new BasicTokenIterator(hit); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token0", ti.next()); @@ -75,16 +74,16 @@ void testSingleHeader() { @Test void testMultiHeader() { final Header[] headers = new Header[]{ - new BasicHeader("Name", "token0,token1"), - new BasicHeader("Name", ""), - new BasicHeader("Name", "token2"), - new BasicHeader("Name", " "), - new BasicHeader("Name", "token3 "), - new BasicHeader("Name", ","), - new BasicHeader("Name", "token4"), + new BasicHeader("Name", "token0,token1"), + new BasicHeader("Name", ""), + new BasicHeader("Name", "token2"), + new BasicHeader("Name", " "), + new BasicHeader("Name", "token3 "), + new BasicHeader("Name", ","), + new BasicHeader("Name", "token4"), }; final Iterator
    hit = new BasicHeaderIterator(headers, null); - final Iterator ti = new BasicTokenIterator(hit); + final Iterator ti = new BasicTokenIterator(hit); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token0", ti.next()); @@ -103,19 +102,19 @@ void testMultiHeader() { @Test void testEmpty() { final Header[] headers = new Header[]{ - new BasicHeader("Name", " "), - new BasicHeader("Name", ""), - new BasicHeader("Name", ","), - new BasicHeader("Name", " ,, "), + new BasicHeader("Name", " "), + new BasicHeader("Name", ""), + new BasicHeader("Name", ","), + new BasicHeader("Name", " ,, "), }; Iterator
    hit = new BasicHeaderIterator(headers, null); - Iterator ti = new BasicTokenIterator(hit); + Iterator ti = new BasicTokenIterator(hit); Assertions.assertFalse(ti.hasNext()); hit = new BasicHeaderIterator(headers, "empty"); - ti = new BasicTokenIterator(hit); + ti = new BasicTokenIterator(hit); Assertions.assertFalse(ti.hasNext()); } @@ -124,15 +123,15 @@ void testEmpty() { @Test void testValueStart() { final Header[] headers = new Header[]{ - new BasicHeader("Name", "token0"), - new BasicHeader("Name", " token1"), - new BasicHeader("Name", ",token2"), - new BasicHeader("Name", " ,token3"), - new BasicHeader("Name", ", token4"), - new BasicHeader("Name", " , token5"), + new BasicHeader("Name", "token0"), + new BasicHeader("Name", " token1"), + new BasicHeader("Name", ",token2"), + new BasicHeader("Name", " ,token3"), + new BasicHeader("Name", ", token4"), + new BasicHeader("Name", " , token5"), }; final Iterator
    hit = new BasicHeaderIterator(headers, null); - final Iterator ti = new BasicTokenIterator(hit); + final Iterator ti = new BasicTokenIterator(hit); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token0", ti.next()); @@ -153,22 +152,22 @@ void testValueStart() { @Test void testValueEnd() { final Header[] headers = new Header[]{ - new BasicHeader("Name", "token0"), - new BasicHeader("Name", "token1 "), - new BasicHeader("Name", "token2,"), - new BasicHeader("Name", "token3 ,"), - new BasicHeader("Name", "token4, "), - new BasicHeader("Name", "token5 , "), + new BasicHeader("Name", "token0"), + new BasicHeader("Name", "token1 "), + new BasicHeader("Name", "token2,"), + new BasicHeader("Name", "token3 ,"), + new BasicHeader("Name", "token4, "), + new BasicHeader("Name", "token5 , "), }; final Iterator
    hit = new BasicHeaderIterator(headers, null); - final Iterator ti = new BasicTokenIterator(hit); + final Iterator ti = new BasicTokenIterator(hit); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token0", ti.next()); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token1", ti.next()); Assertions.assertTrue(ti.hasNext()); - Assertions.assertEquals("token2", ti.next()); + Assertions.assertEquals("token2", ti.next()); Assertions.assertTrue(ti.hasNext()); Assertions.assertEquals("token3", ti.next()); Assertions.assertTrue(ti.hasNext()); @@ -183,13 +182,13 @@ void testWrongPublic() { Assertions.assertThrows(NullPointerException.class, () -> new BasicTokenIterator(null)); final Header[] headers = new Header[]{ - new BasicHeader("Name", " "), - new BasicHeader("Name", ""), - new BasicHeader("Name", ","), - new BasicHeader("Name", " ,, "), + new BasicHeader("Name", " "), + new BasicHeader("Name", ""), + new BasicHeader("Name", ","), + new BasicHeader("Name", " ,, "), }; final Iterator
    hit = new BasicHeaderIterator(headers, null); - final Iterator ti = new BasicTokenIterator(hit); + final Iterator ti = new BasicTokenIterator(hit); Assertions.assertThrows(NoSuchElementException.class, () -> ti.next()); Assertions.assertThrows(UnsupportedOperationException.class, () -> ti.remove()); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedInputBuffer.java b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedInputBuffer.java index 1335d9ca40..38eeb1b5d5 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedInputBuffer.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedInputBuffer.java @@ -27,11 +27,13 @@ package org.apache.hc.core5.http.nio.support.classic; +import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Random; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -194,7 +196,8 @@ void testMultithreadingReadStreamAbort() throws Exception { final Future task2 = executorService.submit((Callable) inputBuffer::read); Assertions.assertEquals(Boolean.TRUE, task1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); - Assertions.assertEquals(Integer.valueOf(-1), task2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); + final ExecutionException ex = Assertions.assertThrows(ExecutionException.class, () -> task2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); + Assertions.assertInstanceOf(InterruptedIOException.class, ex.getCause()); Mockito.verify(capacityChannel, Mockito.never()).update(10); } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedOutputBuffer.java b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedOutputBuffer.java index dd33259aeb..3a03e14d15 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedOutputBuffer.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/support/classic/TestSharedOutputBuffer.java @@ -219,17 +219,35 @@ void testMultithreadingWriteStreamAbort() throws Exception { } @Test - void testEndStreamOnlyCalledOnce() throws IOException { - - final DataStreamChannel channel = Mockito.mock(DataStreamChannel.class); + void testEndStreamOnlyCalledOnce() throws Exception { final SharedOutputBuffer outputBuffer = new SharedOutputBuffer(20); - outputBuffer.flush(channel); + final WritableByteChannelMock channel = new WritableByteChannelMock(1024); + final DataStreamChannelMock dataStreamChannel = Mockito.spy(new DataStreamChannelMock(channel)); - outputBuffer.writeCompleted(); - outputBuffer.flush(channel); + final ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + final Future task1 = executorService.submit(() -> { + outputBuffer.writeCompleted(); + return Boolean.TRUE; + }); + final Future task2 = executorService.submit(() -> { + for (;;) { + outputBuffer.flush(dataStreamChannel); + if (outputBuffer.isEndStream()) { + break; + } + } + return Boolean.TRUE; + }); - Mockito.verify(channel, Mockito.times(1)).endStream(); + Assertions.assertEquals(Boolean.TRUE, task1.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); + Assertions.assertEquals(Boolean.TRUE, task2.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())); + + Mockito.verify(dataStreamChannel, Mockito.times(1)).endStream(); + } finally { + executorService.shutdownNow(); + } } } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestForwardedRequest.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestForwardedRequest.java index c9e51d5903..815324459e 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestForwardedRequest.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestForwardedRequest.java @@ -118,10 +118,9 @@ void testProcessWithIPv6() throws IOException, HttpException { // Check the value of the Forwarded header final String expectedValue = "by=" + remoteAddress.getHostName() + ":" + remoteAddress.getPort() + - ";for=" + localAddress.getHostName() + ":" + localAddress.getPort() + + ";for=" + localAddress.getHostName() + ":" + localAddress.getPort() + ";host=\"" + host + "\";port=" + port + ";proto=" + version.getProtocol(); - assertEquals(expectedValue, request.getFirstHeader(FORWARDED_HEADER_NAME).getValue()); } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java new file mode 100644 index 0000000000..990c4909b1 --- /dev/null +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestRequestTE.java @@ -0,0 +1,199 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TestRequestTE { + + @Test + void testValidTEHeader() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set the TE header and Connection header + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue()); + } + + + @Test + void testMultipleValidTEHeaders() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set both the TE header and the Connection header + request.setHeader(HttpHeaders.TE, "trailers, deflate;q=0.5"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers, deflate;q=0.5", request.getHeader(HttpHeaders.TE).getValue()); + } + + + @Test + void testTEHeaderNotPresent() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + // No TE header, no validation should occur + assertNull(request.getHeader(HttpHeaders.TE)); + } + + @Test + void testTEHeaderContainsChunked() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "chunked"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderInvalidTransferCoding() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "invalid;q=abc"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderAlreadySet() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + final String teValue = "trailers"; + request.setHeader(HttpHeaders.TE, teValue); + request.setHeader(HttpHeaders.CONNECTION, "TE"); // Add the Connection header as required + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(HttpHeaders.TE, request.getHeader(HttpHeaders.TE).getName()); + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals(teValue, request.getHeader(HttpHeaders.TE).getValue()); + } + + + @Test + void testTEHeaderWithConnectionHeaderValidation() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertEquals(HttpHeaders.TE, request.getHeader(HttpHeaders.TE).getName()); + assertNotNull(request.getHeader(HttpHeaders.TE)); + assertEquals("trailers", request.getHeader(HttpHeaders.TE).getValue()); + } + + @Test + void testTEHeaderWithoutConnectionHeaderThrowsException() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + request.setHeader(HttpHeaders.TE, "trailers"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderWithoutTEInConnectionHeaderThrowsException() { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + // Set TE header but Connection header does not include "TE" + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "keep-alive"); // Missing "TE" + + final HttpRequestInterceptor interceptor = new RequestTE(); + Assertions.assertThrows(ProtocolException.class, () -> + interceptor.process(request, request.getEntity(), context)); + } + + @Test + void testTEHeaderWithMultipleDirectivesInConnectionHeader() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set TE header and a Connection header with multiple directives + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "keep-alive, close, TE"); + + final HttpRequestInterceptor interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + assertNotNull(request.getHeader(HttpHeaders.CONNECTION)); + assertTrue(request.getHeader(HttpHeaders.CONNECTION).getValue().contains("TE")); + } + + +} + diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java index bdf336a0df..d0c3d4104b 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java @@ -442,6 +442,25 @@ void testRequestTargetHostConnectHttp10() throws Exception { Assertions.assertNull(header); } + @Test + void testTEHeaderWithConnectionTE() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); + context.setProtocolVersion(HttpVersion.HTTP_1_1); + + // Set both TE and Connection headers as per the requirement + request.setHeader(HttpHeaders.TE, "trailers"); + request.setHeader(HttpHeaders.CONNECTION, "TE"); + + final RequestTE interceptor = new RequestTE(); + interceptor.process(request, request.getEntity(), context); + + final Header connectionHeader = request.getFirstHeader(HttpHeaders.CONNECTION); + Assertions.assertNotNull(connectionHeader); + Assertions.assertEquals("TE", connectionHeader.getValue()); + } + + @Test void testRequestUserAgentGenerated() throws Exception { final HttpCoreContext context = HttpCoreContext.create(); @@ -864,17 +883,6 @@ void testResponseDateGenerated() throws Exception { Assertions.assertNotNull(h2); } - @Test - void testResponseDateNotGenerated() throws Exception { - final HttpCoreContext context = HttpCoreContext.create(); - final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK"); - response.setCode(199); - final ResponseDate interceptor = new ResponseDate(); - interceptor.process(response, response.getEntity(), context); - final Header h1 = response.getFirstHeader(HttpHeaders.DATE); - Assertions.assertNull(h1); - } - @Test void testResponseDateInvalidInput() { final ResponseDate interceptor = new ResponseDate(); @@ -1131,4 +1139,25 @@ void testResponseConformanceNotModifiedWithEntity() { interceptor.process(response, response.getEntity(), context)); } + @Test + void testInvalidDateReplaced() throws Exception { + final HttpCoreContext context = HttpCoreContext.create(); + final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK"); + + // Add an invalid Date header + response.setHeader(HttpHeaders.DATE, "Invalid Date"); + + // Instantiate the ResponseDate interceptor with replaceInvalidDate set to true + final ResponseDate interceptor = new ResponseDate(true); + + // Process the response, which should replace the invalid date + interceptor.process(response, response.getEntity(), context); + + // Assert that the Date header has been replaced + final Header newDateHeader = response.getFirstHeader(HttpHeaders.DATE); + Assertions.assertNotNull(newDateHeader); + Assertions.assertNotEquals("Invalid Date", newDateHeader.getValue()); + } + + } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TLSTest.java b/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TLSTest.java index 4f5b3ef9e0..c7cafa6845 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TLSTest.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TLSTest.java @@ -73,7 +73,7 @@ void parseNull() throws ParseException { @Test void excludeWeakNull() { - assertNull((TLS.excludeWeak((String[]) null))); + assertNull(TLS.excludeWeak((String[]) null)); } @Test diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TestTlsCiphers.java b/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TestTlsCiphers.java index e510af2e9b..082294bcde 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TestTlsCiphers.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/ssl/TestTlsCiphers.java @@ -75,7 +75,7 @@ void testWeakCiphersDisabledByDefault() { } @Test - void excludeH2Blacklisted (){ + void excludeH2Blacklisted () { final String[] mixCipherSuites = { "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", "TLS_RSA_WITH_AES_256_CBC_SHA256", @@ -92,7 +92,7 @@ void excludeH2Blacklisted (){ } @Test - void excludeWeak (){ + void excludeWeak () { final String[] weakCiphersSuites = { "SSL_RSA_WITH_RC4_128_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA", @@ -124,7 +124,7 @@ void excludeWeak (){ } @Test - void excludeWeakNull(){ + void excludeWeakNull() { Assertions.assertNull(TlsCiphers.excludeWeak((String[]) null)); } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/io/TestSocketSupport.java b/httpcore5/src/test/java/org/apache/hc/core5/io/TestSocketSupport.java deleted file mode 100644 index 00bf41a646..0000000000 --- a/httpcore5/src/test/java/org/apache/hc/core5/io/TestSocketSupport.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.core5.io; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketOption; - -import javax.net.ServerSocketFactory; - -import org.apache.hc.core5.util.ReflectionUtils; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class TestSocketSupport { - - @Test - public void testGetExtendedSocketOptionOrNull() { - testGetExtendedSocketOption(SocketSupport.TCP_KEEPIDLE); - testGetExtendedSocketOption(SocketSupport.TCP_KEEPINTERVAL); - testGetExtendedSocketOption(SocketSupport.TCP_KEEPCOUNT); - } - - private void testGetExtendedSocketOption(final String option) { - final SocketOption socketOption = SocketSupport.getExtendedSocketOptionOrNull(option); - // 1.Partial versions of jdk1.8 contain TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT. - // 2. Windows may not support TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT. - if (ReflectionUtils.determineJRELevel() > 8 && !isWindows()) { - Assertions.assertNotNull(socketOption); - } - } - - @Test - public void testSetOption() throws IOException { - if (ReflectionUtils.determineJRELevel() > 8 && isWindows() == false) { - { - // test Socket - final Socket sock = new Socket(); - SocketSupport.setOption(sock, SocketSupport.TCP_KEEPIDLE, 20); - SocketSupport.setOption(sock, SocketSupport.TCP_KEEPINTERVAL, 21); - SocketSupport.setOption(sock, SocketSupport.TCP_KEEPCOUNT, 22); - - final SocketOption tcpKeepIdle = SocketSupport.getExtendedSocketOptionOrNull(SocketSupport.TCP_KEEPIDLE); - assert tcpKeepIdle != null; - Assertions.assertEquals(20, ReflectionUtils.callGetter(sock, "Option", tcpKeepIdle, SocketOption.class, Integer.class)); - - final SocketOption tcpKeepInterval = SocketSupport.getExtendedSocketOptionOrNull(SocketSupport.TCP_KEEPINTERVAL); - assert tcpKeepInterval != null; - Assertions.assertEquals(21, ReflectionUtils.callGetter(sock, "Option", tcpKeepInterval, SocketOption.class, Integer.class)); - - final SocketOption tcpKeepCount = SocketSupport.getExtendedSocketOptionOrNull(SocketSupport.TCP_KEEPCOUNT); - assert tcpKeepCount != null; - Assertions.assertEquals(22, ReflectionUtils.callGetter(sock, "Option", tcpKeepCount, SocketOption.class, Integer.class)); - } - - { - // test ServerSocket - final ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(); - SocketSupport.setOption(serverSocket, SocketSupport.TCP_KEEPIDLE, 20); - SocketSupport.setOption(serverSocket, SocketSupport.TCP_KEEPINTERVAL, 21); - SocketSupport.setOption(serverSocket, SocketSupport.TCP_KEEPCOUNT, 22); - - final SocketOption tcpKeepIdle = SocketSupport.getExtendedSocketOptionOrNull(SocketSupport.TCP_KEEPIDLE); - assert tcpKeepIdle != null; - Assertions.assertEquals(20, ReflectionUtils.callGetter(serverSocket, "Option", tcpKeepIdle, SocketOption.class, Integer.class)); - - final SocketOption tcpKeepInterval = SocketSupport.getExtendedSocketOptionOrNull(SocketSupport.TCP_KEEPINTERVAL); - assert tcpKeepInterval != null; - Assertions.assertEquals(21, ReflectionUtils.callGetter(serverSocket, "Option", tcpKeepInterval, SocketOption.class, Integer.class)); - - final SocketOption tcpKeepCount = SocketSupport.getExtendedSocketOptionOrNull(SocketSupport.TCP_KEEPCOUNT); - assert tcpKeepCount != null; - Assertions.assertEquals(22, ReflectionUtils.callGetter(serverSocket, "Option", tcpKeepCount, SocketOption.class, Integer.class)); - } - } - } - - public static boolean isWindows() { - return System.getProperty("os.name").contains("Windows"); - } - -} diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java index 45dc8229dc..74e7a6016c 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java @@ -171,7 +171,7 @@ void testFormatQuery() { @Test void testHierarchicalUri() throws Exception { final URI uri = new URI("http", "stuff", "localhost", 80, "/some stuff", "param=stuff", "fragment"); - final URIBuilder uribuilder = new URIBuilder(uri); + final URIBuilder uribuilder = new URIBuilder(uri).setEncodingPolicy(URIBuilder.EncodingPolicy.ALL_RESERVED); final URI result = uribuilder.build(); Assertions.assertEquals(new URI("http://stuff@localhost:80/some%20stuff?param=stuff#fragment"), result); } @@ -220,56 +220,48 @@ void testHierarchicalUriMutation() throws Exception { @Test void testLocalhost() throws Exception { - // Check that the URI generated by URI builder agrees with that generated by using URI directly - final String scheme="https"; - final InetAddress host=InetAddress.getLocalHost(); - final String specials="/abcd!$&*()_-+.,=:;'~@[]?<>|#^%\"{}\\\u00a3`\u00ac\u00a6xyz"; // N.B. excludes space - final URI uri = new URI(scheme, specials, host.getHostAddress(), 80, specials, specials, specials); - - final URI bld = URIBuilder.localhost() - .setScheme(scheme) - .setUserInfo(specials) - .setPath(specials) - .setCustomQuery(specials) - .setFragment(specials) - .build(); - - Assertions.assertEquals(uri.getHost(), bld.getHost()); - - Assertions.assertEquals(uri.getUserInfo(), bld.getUserInfo()); - - Assertions.assertEquals(uri.getPath(), bld.getPath()); + // Check that the URI generated by URI builder agrees with that generated by using URI directly + final String scheme = "https"; + final InetAddress host = InetAddress.getLocalHost(); + final String specials = "/abcd!$&*()_-+.,=:;'~@[]?<>|#^%\"{}\\\u00a3`\u00ac\u00a6xyz"; // N.B. excludes space + final URI uri = new URI(scheme, specials, host.getHostAddress(), 80, specials, specials, specials); - Assertions.assertEquals(uri.getQuery(), bld.getQuery()); + final URI bld = URIBuilder.localhost() + .setScheme(scheme) + .setUserInfo(specials) + .setPath(specials) + .setCustomQuery(specials) + .setFragment(specials) + .build(); - Assertions.assertEquals(uri.getFragment(), bld.getFragment()); + Assertions.assertEquals(uri.getHost(), bld.getHost()); + Assertions.assertEquals(uri.getUserInfo(), bld.getUserInfo()); + Assertions.assertEquals(uri.getPath(), bld.getPath()); + Assertions.assertEquals(uri.getQuery(), bld.getQuery()); + Assertions.assertEquals(uri.getFragment(), bld.getFragment()); } @Test void testLoopbackAddress() throws Exception { - // Check that the URI generated by URI builder agrees with that generated by using URI directly - final String scheme="https"; - final InetAddress host=InetAddress.getLoopbackAddress(); - final String specials="/abcd!$&*()_-+.,=:;'~@[]?<>|#^%\"{}\\\u00a3`\u00ac\u00a6xyz"; // N.B. excludes space - final URI uri = new URI(scheme, specials, host.getHostAddress(), 80, specials, specials, specials); - - final URI bld = URIBuilder.loopbackAddress() - .setScheme(scheme) - .setUserInfo(specials) - .setPath(specials) - .setCustomQuery(specials) - .setFragment(specials) - .build(); - - Assertions.assertEquals(uri.getHost(), bld.getHost()); - - Assertions.assertEquals(uri.getUserInfo(), bld.getUserInfo()); - - Assertions.assertEquals(uri.getPath(), bld.getPath()); + // Check that the URI generated by URI builder agrees with that generated by using URI directly + final String scheme = "https"; + final InetAddress host = InetAddress.getLoopbackAddress(); + final String specials = "/abcd!$&*()_-+.,=:;'~@[]?<>|#^%\"{}\\\u00a3`\u00ac\u00a6xyz"; // N.B. excludes space + final URI uri = new URI(scheme, specials, host.getHostAddress(), 80, specials, specials, specials); - Assertions.assertEquals(uri.getQuery(), bld.getQuery()); + final URI bld = URIBuilder.loopbackAddress() + .setScheme(scheme) + .setUserInfo(specials) + .setPath(specials) + .setCustomQuery(specials) + .setFragment(specials) + .build(); - Assertions.assertEquals(uri.getFragment(), bld.getFragment()); + Assertions.assertEquals(uri.getHost(), bld.getHost()); + Assertions.assertEquals(uri.getUserInfo(), bld.getUserInfo()); + Assertions.assertEquals(uri.getPath(), bld.getPath()); + Assertions.assertEquals(uri.getQuery(), bld.getQuery()); + Assertions.assertEquals(uri.getFragment(), bld.getFragment()); } @Test @@ -473,12 +465,23 @@ void testPathEncoding() throws Exception { Assertions.assertEquals(uri1, uri2); } + @Test + void testFragmentEncoding() throws Exception { + final URI uri1 = new URI("https://somehost.com#some%20fragment%20with%20all%20sorts%20of%20$tuff%20in%20it!!!"); + final URI uri2 = new URIBuilder() + .setScheme("https") + .setHost("somehost.com") + .setFragment("some fragment with all sorts of $tuff in it!!!") + .build(); + Assertions.assertEquals(uri1, uri2); + } + @Test void testAgainstURI() throws Exception { // Check that the URI generated by URI builder agrees with that generated by using URI directly - final String scheme="https"; - final String host="localhost"; - final String specials="/abcd!$&*()_-+.,=:;'~@[]?<>|#^%\"{}\\\u00a3`\u00ac\u00a6xyz"; // N.B. excludes space + final String scheme = "https"; + final String host = "localhost"; + final String specials = "/abcd!$&*()_-+.,=:;'~@[]?<>|#^%\"{}\\\u00a3`\u00ac\u00a6xyz"; // N.B. excludes space final URI uri = new URI(scheme, specials, host, 80, specials, specials, specials); final URI bld = new URIBuilder() @@ -491,15 +494,10 @@ void testAgainstURI() throws Exception { .build(); Assertions.assertEquals(uri.getHost(), bld.getHost()); - Assertions.assertEquals(uri.getUserInfo(), bld.getUserInfo()); - Assertions.assertEquals(uri.getPath(), bld.getPath()); - Assertions.assertEquals(uri.getQuery(), bld.getQuery()); - Assertions.assertEquals(uri.getFragment(), bld.getFragment()); - } @Test @@ -973,4 +971,46 @@ void testHttpUriWithEmptyHost() { .setFragment("fragment"); Assertions.assertThrows(URISyntaxException.class, uribuilder::build); } + + @Test + void testSetPlusAsBlank() throws Exception { + // Case 1: Plus as blank, "+" should be treated as space + URIBuilder uriBuilder = new URIBuilder("http://localhost?param=hello+world") + .setPlusAsBlank(true); + List params = uriBuilder.getQueryParams(); + Assertions.assertEquals("hello world", params.get(0).getValue()); + + // Case 2: Plus as plus, "+" should remain "+" + uriBuilder = new URIBuilder("http://localhost?param=hello+world") + .setPlusAsBlank(false); + params = uriBuilder.getQueryParams(); + Assertions.assertEquals("hello+world", params.get(0).getValue()); + + // Case 3: '%20' always interpreted as space + uriBuilder = new URIBuilder("http://localhost?param=hello%20world") + .setPlusAsBlank(true); + params = uriBuilder.getQueryParams(); + Assertions.assertEquals("hello world", params.get(0).getValue()); + + uriBuilder = new URIBuilder("http://localhost?param=hello%20world") + .setPlusAsBlank(false); + params = uriBuilder.getQueryParams(); + Assertions.assertEquals("hello world", params.get(0).getValue()); + } + + @Test + void testCustomQueryEncoding() throws Exception { + final String query = "query param:!@/?\""; + final String expectedEncodedQuery = "query%20param:!@/?%22"; + + final URI uri = new URIBuilder() + .setScheme("http") + .setHost("example.com") + .setCustomQuery(query) + .setEncodingPolicy(URIBuilder.EncodingPolicy.RFC_3986) + .build(); + + Assertions.assertEquals(expectedEncodedQuery, uri.getRawQuery()); + } + } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestLaxConnPool.java b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestLaxConnPool.java index 29263c6a03..c2e01c065b 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestLaxConnPool.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestLaxConnPool.java @@ -156,7 +156,8 @@ void testLeaseInvalid() { try (final LaxConnPool pool = new LaxConnPool<>(2)) { Assertions.assertThrows(NullPointerException.class, () -> pool.lease(null, null, Timeout.ZERO_MILLISECONDS, null)); - }} + } + } @Test void testReleaseUnknownEntry() { diff --git a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestStrictConnPool.java b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestStrictConnPool.java index 08b7798bdd..d63c4acaa8 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestStrictConnPool.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestStrictConnPool.java @@ -339,7 +339,8 @@ void testConnectionRedistributionOnTotalMaxLimit() throws Exception { totals = pool.getTotalStats(); Assertions.assertEquals(2, totals.getAvailable()); Assertions.assertEquals(0, totals.getLeased()); - Assertions.assertEquals(0, totals.getPending());} + Assertions.assertEquals(0, totals.getPending()); + } } @Test diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOWorkerSelectorsTest.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOWorkerSelectorsTest.java new file mode 100644 index 0000000000..3910445627 --- /dev/null +++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOWorkerSelectorsTest.java @@ -0,0 +1,51 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.reactor; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +class IOWorkerSelectorsTest { + + @ParameterizedTest(name = "worker count = {0}") + @ValueSource(ints = {1, 2, 3, 4, 5, 10, 15, 16, 32}) + void testIndexOverflow(final int workerCount) { + final long start = (long) Integer.MAX_VALUE - 10; + final long end = (long) Integer.MAX_VALUE + 10; + final IOWorkerStats[] workers = new IOWorkerStats[workerCount]; + for (int i = 0; i < workerCount; i++) { + workers[i] = Mockito.mock(IOWorkerStats.class); + } + final IOWorkerSelector selector = IOWorkerSelectors.newSelector(workerCount, (int) start); + for (long i = start; i < end; i++) { + Assertions.assertTrue(selector.select(workers) < workerCount); + } + } + +} diff --git a/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java index b4f7a469be..6da19a1d18 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/ssl/TestSSLContextBuilder.java @@ -257,10 +257,10 @@ void testBuildKSWithProviderName() throws Exception { @Test void testBuildTSWithNoSuchProvider() { - Assertions.assertThrows(NoSuchProviderException.class, ()-> - SSLContextBuilder.create() - .setTrustStoreProvider("no-such-provider") - .build()); + Assertions.assertThrows(NoSuchProviderException.class, () -> + SSLContextBuilder.create() + .setTrustStoreProvider("no-such-provider") + .build()); } @Test diff --git a/httpcore5/src/test/java/org/apache/hc/core5/util/TestDeadline.java b/httpcore5/src/test/java/org/apache/hc/core5/util/TestDeadline.java index 9677ad107d..d4e6b4784f 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/util/TestDeadline.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/util/TestDeadline.java @@ -130,4 +130,14 @@ void testValue() { final Deadline deadline = Deadline.fromUnixMilliseconds(nowPlusOneMin); Assertions.assertEquals(nowPlusOneMin, deadline.getValue()); } + + @Test + void testOverflowHandling() { + final long currentTime = Long.MAX_VALUE - 5000; // Simulate close to overflow + final TimeValue tenSeconds = TimeValue.ofMilliseconds(10000); // 10 seconds + final Deadline deadline = Deadline.calculate(currentTime, tenSeconds); + + Assertions.assertEquals(Deadline.MAX_VALUE, deadline, + "Overflow should result in the maximum deadline value."); + } } diff --git a/mvnw b/mvnw new file mode 100755 index 0000000000..14c58dcb00 --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000000..249bdf3822 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 793b8891c7..9ea2647b85 100644 --- a/pom.xml +++ b/pom.xml @@ -27,15 +27,15 @@ org.apache.httpcomponents httpcomponents-parent - 13 + 14 4.0.0 org.apache.httpcomponents.core5 httpcore5-parent Apache HttpComponents Core Parent - 5.3.1-SNAPSHOT + 5.4-alpha1-SNAPSHOT Apache HttpComponents Core is a library of components for building HTTP enabled services - https://hc.apache.org/httpcomponents-core-5.3.x/${project.version}/ + https://hc.apache.org/httpcomponents-core-5.4.x/${project.version}/ 2005 pom @@ -48,14 +48,14 @@ scm:git:https://gitbox.apache.org/repos/asf/httpcomponents-core.git scm:git:https://gitbox.apache.org/repos/asf/httpcomponents-core.git https://github.com/apache/httpcomponents-core/tree/${project.scm.tag} - 5.3.1-SNAPSHOT + 5.4-alpha1-SNAPSHOT apache.website Apache HttpComponents Website - scm:svn:https://svn.apache.org/repos/asf/httpcomponents/site/components/httpcomponents-core-5.3.x/LATEST/ + scm:svn:https://svn.apache.org/repos/asf/httpcomponents/site/components/httpcomponents-core-5.4.x/LATEST/ @@ -72,16 +72,16 @@ 1.8 true 2.5.2 - 5.11.0 + 5.13.3 3.0 5.0.0 4.11.0 1.7.36 - 2.23.1 - 2.2.21 - 3.1.9 + 2.25.0 + 3.1.10 + 1.21.3 5.3 - javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer + javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer,jdk.net.ExtendedSocketOptions,jdk.net.Sockets @@ -151,6 +151,16 @@ log4j-core ${log4j.version} + + org.testcontainers + testcontainers + ${testcontainers.version} + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + @@ -221,6 +231,7 @@ true true + true METHOD_NEW_DEFAULT @@ -230,6 +241,7 @@ @org.apache.hc.core5.annotation.Internal + org.apache.hc.core5.testing.reactive.ReactiveTestUtils @@ -358,7 +370,6 @@ @org.apache.hc.core5.annotation.Internal - true @@ -371,4 +382,4 @@ - \ No newline at end of file + diff --git a/test-CA/ca-cert.pem b/test-CA/ca-cert.pem new file mode 100644 index 0000000000..a40c7631b7 --- /dev/null +++ b/test-CA/ca-cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyTCCArGgAwIBAgIJAO3mCIu9mboMMA0GCSqGSIb3DQEBCwUAMHoxIzAhBgNV +BAoMGkFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMR8wHQYDVQQLDBZIdHRwQ29t +cG9uZW50cyBQcm9qZWN0MRAwDgYDVQQDDAdUZXN0IENBMSAwHgYJKoZIhvcNAQkB +FhFkZXZAaGMuYXBhY2hlLm9yZzAgFw0xNDEwMTMxNTAxMjBaGA8yMjg4MDcyODE1 +MDEyMFowejEjMCEGA1UECgwaQXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xHzAd +BgNVBAsMFkh0dHBDb21wb25lbnRzIFByb2plY3QxEDAOBgNVBAMMB1Rlc3QgQ0Ex +IDAeBgkqhkiG9w0BCQEWEWRldkBoYy5hcGFjaGUub3JnMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEApXhHtKRvAxbLI+f21zNe68dkVXAhSMIfHQJGb2en +S1H8yE4HPIb4vPQ0U7fQCb7RXplm6cHExpof4cO3DmyqD5KeQk0TdM8XrhviDgwj +Y0KQ/lgwGHR5CpYoZ6LYWaLSE/wt9dVu80UcK8a3hW9G0X/4b79fMO6HYDix+CI4 +b17sqZ4K0tWKA10Xe+2RJU8Y01pPBaPR/UsAn+a1pZ6f8BhL879oWHfLWKcgZOYP +U4sYED0S8gs4/ED1zRj2/uHb313sHTl+OU4X5v+OvwBvbNBrl5qfMTZnRNxlOfRq +UTJdcopsp2aNeqHiorSDOrHwMIJpxQ2XqHT2l9s8msXf4wIDAQABo1AwTjAdBgNV +HQ4EFgQUA+Tn2g9k2xMevYWrdrwpyi+nx0swHwYDVR0jBBgwFoAUA+Tn2g9k2xMe +vYWrdrwpyi+nx0swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAFVEp +8Nv6JoFY7oGgu6068fH/kq7A3SllqMpbv7dddI9fgwy352cBtg6PkYkGtEE037xs +FQSYV1NiAkNWTJER+Q+kVbQrhuPNKZqh1g0sUKwv3X20BmgJ9hbU9klWZjdjujyd +h9Ybjuntkn5XPp1zN6zHD0sQReEJnRlD6FT1axrQWpICzE4qoo8k64G+6/rqFywc +oMc/Of3KCAHjtbWklEu97hjBvGC/nEP4/VhRrjWWSeGHv88LCyO/Yg6v3zrZHFLW ++KhsDCPyLxSSISFskLQfukiqf2lr87kQq/oF27sAr3sR3Jqh4qzflM2XLgjmZuRE +OrHT6lvUemRyksA5qg== +-----END CERTIFICATE----- diff --git a/test-CA/ca-key.pem b/test-CA/ca-key.pem new file mode 100644 index 0000000000..a8a304241e --- /dev/null +++ b/test-CA/ca-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIq0bLh96mWv4CAggA +MBQGCCqGSIb3DQMHBAimZqUiELx13QSCBMgaLWrGFqveIzwQUsebS6FBdVq0lodz +Vlekje8ycFDYSd21V9jPMwrSupZceeBQjCrpyLZ3oPkR+MvObmznev8XYcJzVCkF +E9ApAaHZe248wWcu1/D7auHNG3GyZfvYS0c//Rs2OzMZfsUvX93RVullCRREvCYS +qXhaO3ywFocndKRpSnkOBs2SRa0yc9POl4n4dwyKhsJUaMSmhPbJr9UBvCbXHZIA +gLcSWzVon3EtZCSubMp9eo90G5MzIXEyPBTcIHwpyqRWTkaTUTq4R0c4/RTX+l7K +OZuRIEeBEW6z009fSagymN/KEH3gUkg5pG6i1YWF63OVKTMGn+yQGWwYXwTyEGi5 +HZpD98wh3ycucmL93XLk+yYXQcTp1i+u4GaXNWGREQvNW6onCGeg6WWj1PrIsqoi +TZ2pgQUJWPR1K3037hY0o9sakAkyYSyTPVvHOUcbf3+GhqGS1FsSNOxKRNpYm/3v +Gf0SUN8BavPliK9NSU5JAbprr/hoL5o72dCX9DiOgwfW3HyD/gLh7sVyVBdAzTnE +XFaYFnrb5QnqHbgWvaLbJUT5K7MW3OFLVConydYtYdaUl5z49OflhgnvYOPgTSUr +k9c7exQjedAduPd8dXODh9l2g+QEXJoT+YYFEYHkQlsZgH1hCLXD1TmAeI4LMklb +vPaGE8Ouj1pfbejdTNsqLfW0IiR/jZzEjRgqrueMf2VUjtqTZyPayc2rU4kOoKhv +JzQ0wOFhgRztWJy2voRe+iYss3ToqZ7qLpjBfCTsxCJqbuaGeJWWSnOlDpSysgr+ +q4BvCzDcvf/0mKD2cQuJx/kynQMCcWB/VegRsQ24Y+3T7IU1w8ccmRfSZ93AwkAh +MKJzKaVhD/gn9vUG/we18p7RMIc9pk1o2Z2Ru3mKjkO3QYRP6Y7yk0ah2JKrHIPf +LWfPuHmtzHQXkY3RbVvxvwD/+qHm8ogXq52w8cpGhY5UwAEHrLLwypdBHccrAJjo +bE13M/MrtTry/k8OMRqhhRzHUXBq6mLaWffCaP2SAVfJEez2iASvGJFvgy3bSkWY +rwWMSfZKDkauwDMW5gpFrpeuqgD64LO72sN01riVDpaEyNODRCEEBGce+O+91R9K +TLVgRYFsxClyZy1nynD66gkTepEm1yOgcdqV3651Os+TGm39jGYHy1k9mPz8ypqf +8n8uw4nV3SbIwfpy4Z8onHixfc/Fugm7yQHW4dSuCpahyIJHom6Cq7SZfPuo9e3t +8tqaxvK4U/dAXoimvN1eakH2FoVFIj3mk7OAKBgmDINH9GlzXPwRsTfiJSP4Xaod +ouWIQLLeXQuuOc5VJd1Xex75o8ciSOomAS0uR4Fvk/2NkAm0EMddjZnuWLQaXPry +JiUIgSx3w3yRq9RSQOxDRQpp2nP2roX7cyeGPzTmeujikExGTa3YBxuAShDLx5pt +fpi0ol8H8ohDU4eV9pv96KRBG9e8sQf1zpGjeYLTFiN35IQxYJx3HTXp9/oFWkmA +OdCEwggIKJ/RtgkWOWogTilQVA41p4XZr661fxoSE86sHXkZKn8IGnAKLFT46nWM +IYVDalYUiSNZr+KbzmLIV3LmYE3mlqGI4vDvQtd9zQk/uatYBc2DetuTWPZHCEKS +3Nk= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test-CA/openssl.cnf b/test-CA/openssl.cnf new file mode 100644 index 0000000000..b1c8257fe5 --- /dev/null +++ b/test-CA/openssl.cnf @@ -0,0 +1,357 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] + +# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +# Policies used by the TSA examples. +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +dir = ./test-CA # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several ctificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/ca-cert.pem # The CA certificate +serial = $dir/serial.txt # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/ca-key.pem# The private key +RANDFILE = $dir/.rand # private random number file + +x509_extensions = usr_cert # The extentions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = default # use public key default MD +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = optional +stateOrProvinceName = optional +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extentions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString (PKIX recommendation before 2004) +# utf8only: only UTF8Strings (PKIX recommendation after 2004). +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. +string_mask = utf8only + +# req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Some-State + +localityName = Locality Name (eg, city) + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Apache Software Foundation + +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = HttpComponents Project + +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This is required for TSA certificates. +# extendedKeyUsage = critical,timeStamping + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ v3_ca ] + + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo + +#################################################################### +[ tsa ] + +default_tsa = tsa_config1 # the default TSA section + +[ tsa_config1 ] + +# These are used by the TSA reply generation only. +dir = ./demoCA # TSA root directory +serial = $dir/tsaserial # The current serial number (mandatory) +crypto_device = builtin # OpenSSL engine to use for signing +signer_cert = $dir/tsacert.pem # The TSA signing certificate + # (optional) +certs = $dir/cacert.pem # Certificate chain to include in reply + # (optional) +signer_key = $dir/private/tsakey.pem # The TSA private key (optional) + +default_policy = tsa_policy1 # Policy if request did not specify it + # (optional) +other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) +digests = md5, sha1 # Acceptable message digests (mandatory) +accuracy = secs:1, millisecs:500, microsecs:100 # (optional) +clock_precision_digits = 0 # number of digits after dot. (optional) +ordering = yes # Is ordering defined for timestamps? + # (optional, default: no) +tsa_name = yes # Must the TSA name be included in the reply? + # (optional, default: no) +ess_cert_id_chain = no # Must the ESS cert id chain be included? + # (optional, default: no)