diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d8663fab1a..4d1e8a8be3d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -327,8 +327,8 @@ jobs: uses: ./.github/workflows/build-macos.yml with: platform: macos-x64 - runs-on: 'macos-13' - xcode-toolset-version: '14.3.1' + runs-on: 'macos-15-intel' + xcode-toolset-version: '16.4' configure-arguments: ${{ github.event.inputs.configure-arguments }} make-arguments: ${{ github.event.inputs.make-arguments }} dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} @@ -340,8 +340,8 @@ jobs: uses: ./.github/workflows/build-macos.yml with: platform: macos-aarch64 - runs-on: 'macos-14' - xcode-toolset-version: '15.4' + runs-on: 'macos-15' + xcode-toolset-version: '16.4' configure-arguments: ${{ github.event.inputs.configure-arguments }} make-arguments: ${{ github.event.inputs.make-arguments }} dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} @@ -432,9 +432,9 @@ jobs: with: platform: macos-aarch64 bootjdk-platform: macos-aarch64 - runs-on: macos-14 + runs-on: macos-15 dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} - xcode-toolset-version: '15.4' + xcode-toolset-version: '16.4' debug-suffix: -debug test-windows-x64: diff --git a/doc/hotspot-style.html b/doc/hotspot-style.html index fb4cffc9d43..f1c25dab7f4 100644 --- a/doc/hotspot-style.html +++ b/doc/hotspot-style.html @@ -86,8 +86,9 @@ values
  • thread_local
  • nullptr
  • <atomic>
  • -
  • Inline -Variables
  • +
  • Variable Templates and +Inline Variables
  • Initializing variables with static storage duration
  • @@ -937,12 +938,18 @@ differ from what the Java compilers implement.

    "conservative" memory ordering, which may differ from (may be stronger than) sequentially consistent. There are algorithms in HotSpot that are believed to rely on that ordering.

    -

    Inline Variables

    -

    Variables with static storage duration may be declared -inline (p0386r2). -This has similar effects as for declaring a function inline: it can be -defined, identically, in multiple translation units, must be defined in -every translation unit in which it is Variable Templates and +Inline Variables +

    The use of variable templates (including static data member +templates) (N3651) is permitted. +They provide parameterized variables and constants in a simple and +direct form, instead of requiring the use of various workarounds.

    +

    Variables with static storage duration and variable templates may be +declared inline (p0386r2), and this usage is +permitted. This has similar effects as for declaring a function inline: +it can be defined, identically, in multiple translation units, must be +defined in every translation unit in which it is ODR used, and the behavior of the program is as if there is exactly one variable.

    @@ -955,16 +962,17 @@ initializations can make initialization order problems worse. The few ordering constraints that exist for non-inline variables don't apply, as there isn't a single program-designated translation unit containing the definition.

    -

    A constexpr static data member is implicitly -inline. As a consequence, an A constexpr static data member or static data member +template is implicitly inline. As a consequence, an ODR use of such a variable doesn't -require a definition in some .cpp file. (This is a change from -pre-C++17. Beginning with C++17, such a definition is considered a -duplicate definition, and is deprecated.)

    -

    Declaring a thread_local variable inline is -forbidden for HotSpot code. The use of -thread_local is already heavily restricted.

    +title="One Definition Rule">ODR use of such a member doesn't require +a definition in some .cpp file. (This is a change from pre-C++17. +Beginning with C++17, such a definition is considered a duplicate +definition, and is deprecated.)

    +

    Declaring a thread_local variable template or +inline variable is forbidden in HotSpot code. The use of thread_local is already +heavily restricted.

    Initializing variables with static storage duration

    @@ -1851,11 +1859,6 @@ difference.

    Additional Undecided Features

    - + * * This ensures that the natural ordering of {@code Double} * objects imposed by this method is consistent with * equals; see {@linkplain ##equivalenceRelation this * discussion for details of floating-point comparison and * ordering}. * + * @apiNote + * The inclusion of a total order idiom in the Java SE API + * predates the inclusion of that functionality in the IEEE 754 + * standard. The ordering of the totalOrder predicate chosen by + * IEEE 754 differs from the total order chosen by this method. + * While this method treats all NaN representations as being in + * the same equivalence class, the IEEE 754 total order defines an + * ordering based on the bit patterns of the NaN among the + * different NaN representations. The IEEE 754 order regards + * "negative" NaN representations, that is NaN representations + * whose sign bit is set, to be less than any finite or infinite + * value and less than any "positive" NaN. In addition, the IEEE + * order regards all positive NaN values as greater than positive + * infinity. See the IEEE 754 standard for full details of its + * total ordering. + * * @param anotherDouble the {@code Double} to be compared. * @return the value {@code 0} if {@code anotherDouble} is * numerically equal to this {@code Double}; a value @@ -1455,6 +1473,12 @@ public final class Double extends Number * Double.valueOf(d1).compareTo(Double.valueOf(d2)) * * + * @apiNote + * One idiom to implement {@linkplain ##repEquivalence + * representation equivalence} on {@code double} values is + * {@snippet lang="java" : + * Double.compare(a, b) == 0 + * } * @param d1 the first {@code double} to compare * @param d2 the second {@code double} to compare * @return the value {@code 0} if {@code d1} is diff --git a/src/java.base/share/classes/java/lang/Float.java b/src/java.base/share/classes/java/lang/Float.java index 4344d9657b4..db694571567 100644 --- a/src/java.base/share/classes/java/lang/Float.java +++ b/src/java.base/share/classes/java/lang/Float.java @@ -420,10 +420,12 @@ public final class Float extends Number // replace subnormal double exponent with subnormal float // exponent String s = Double.toHexString(Math.scalb((double)f, - /* -1022+126 */ - Double.MIN_EXPONENT- + // -1022 + 126 + Double.MIN_EXPONENT - Float.MIN_EXPONENT)); - return s.replaceFirst("p-1022$", "p-126"); + // The char sequence "-1022" can only appear in the + // representation of the exponent, not in the (hex) significand. + return s.replace("-1022", "-126"); } else // double string will be the same as float string return Double.toHexString(f); @@ -871,6 +873,9 @@ public final class Float extends Number * same if and only if the method {@link #floatToIntBits(float)} * returns the identical {@code int} value when applied to * each. + * In other words, {@linkplain Double##repEquivalence + * representation equivalence} is used to compare the {@code + * float} values. * * @apiNote * This method is defined in terms of {@link @@ -1250,6 +1255,10 @@ public final class Float extends Number * discussion for details of floating-point comparison and * ordering}. * + * @apiNote + * For a discussion of differences between the total order of this + * method compared to the total order defined by the IEEE 754 + * standard, see the note in {@link Double#compareTo(Double)}. * * @param anotherFloat the {@code Float} to be compared. * @return the value {@code 0} if {@code anotherFloat} is @@ -1276,6 +1285,14 @@ public final class Float extends Number * Float.valueOf(f1).compareTo(Float.valueOf(f2)) * * + * @apiNote + * One idiom to implement {@linkplain + * Double##repEquivalence representation equivalence} on {@code + * float} values is + * {@snippet lang="java" : + * Float.compare(a, b) == 0 + * } + * * @param f1 the first {@code float} to compare. * @param f2 the second {@code float} to compare. * @return the value {@code 0} if {@code f1} is diff --git a/src/java.base/share/classes/java/lang/Record.java b/src/java.base/share/classes/java/lang/Record.java index 808bc7cc9cd..f15ee07a964 100644 --- a/src/java.base/share/classes/java/lang/Record.java +++ b/src/java.base/share/classes/java/lang/Record.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -127,7 +127,12 @@ public abstract class Record { * * * - * Apart from the semantics described above, the precise algorithm + * Note that these rules imply that {@linkplain + * Double##repEquivalence representation equivalence} is used for + * the equality comparison of both primitive floating-point values + * and wrapped floating-point values. + * + *

    Apart from the semantics described above, the precise algorithm * used in the implicitly provided implementation is unspecified * and is subject to change. The implementation may or may not use * calls to the particular methods listed, and may or may not diff --git a/src/java.base/share/classes/java/lang/Shutdown.java b/src/java.base/share/classes/java/lang/Shutdown.java index 36cf471a575..87c4732a5ce 100644 --- a/src/java.base/share/classes/java/lang/Shutdown.java +++ b/src/java.base/share/classes/java/lang/Shutdown.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -157,8 +157,10 @@ class Shutdown { * which should pass a nonzero status code. */ static void exit(int status) { - logRuntimeExit(status); // Log without holding the lock; - + // log only if VM is fully initialized + if (VM.isBooted()) { + logRuntimeExit(status); // Log without holding the lock; + } synchronized (Shutdown.class) { /* Synchronize on the class object, causing any other thread * that attempts to initiate shutdown to stall indefinitely diff --git a/src/java.base/share/classes/java/lang/String.java b/src/java.base/share/classes/java/lang/String.java index 24ead22e283..a18ac3250dc 100644 --- a/src/java.base/share/classes/java/lang/String.java +++ b/src/java.base/share/classes/java/lang/String.java @@ -3710,7 +3710,7 @@ public final class String if (len < 0L || (len <<= coder) != (int) len) { throw new OutOfMemoryError("Requested string length exceeds VM limit"); } - byte[] value = StringConcatHelper.newArray(len); + byte[] value = StringConcatHelper.newArray((int) len); int off = 0; prefix.getBytes(value, off, coder); off += prefix.length(); diff --git a/src/java.base/share/classes/java/lang/StringConcatHelper.java b/src/java.base/share/classes/java/lang/StringConcatHelper.java index a5f6abbfdf1..aeff494cabb 100644 --- a/src/java.base/share/classes/java/lang/StringConcatHelper.java +++ b/src/java.base/share/classes/java/lang/StringConcatHelper.java @@ -141,262 +141,6 @@ final class StringConcatHelper { // no instantiation } - /** - * Return the coder for the character. - * @param value character - * @return coder - */ - static long coder(char value) { - return StringLatin1.canEncode(value) ? LATIN1 : UTF16; - } - - /** - * Check for overflow, throw exception on overflow. - * - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @return the given parameter value, if valid - */ - private static long checkOverflow(long lengthCoder) { - if ((int)lengthCoder >= 0) { - return lengthCoder; - } - throw new OutOfMemoryError("Overflow: String length out of range"); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, boolean value) { - return checkOverflow(lengthCoder + (value ? 4 : 5)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, char value) { - return checkOverflow(lengthCoder + 1) | coder(value); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, int value) { - return checkOverflow(lengthCoder + DecimalDigits.stringSize(value)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, long value) { - return checkOverflow(lengthCoder + DecimalDigits.stringSize(value)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, String value) { - lengthCoder += value.length(); - if (!value.isLatin1()) { - lengthCoder |= UTF16; - } - return checkOverflow(lengthCoder); - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value boolean value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, boolean value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - if (value) { - index -= 4; - buf[index] = 't'; - buf[index + 1] = 'r'; - buf[index + 2] = 'u'; - buf[index + 3] = 'e'; - } else { - index -= 5; - buf[index] = 'f'; - buf[index + 1] = 'a'; - buf[index + 2] = 'l'; - buf[index + 3] = 's'; - buf[index + 4] = 'e'; - } - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - if (value) { - index -= 4; - StringUTF16.putChar(buf, index, 't'); - StringUTF16.putChar(buf, index + 1, 'r'); - StringUTF16.putChar(buf, index + 2, 'u'); - StringUTF16.putChar(buf, index + 3, 'e'); - } else { - index -= 5; - StringUTF16.putChar(buf, index, 'f'); - StringUTF16.putChar(buf, index + 1, 'a'); - StringUTF16.putChar(buf, index + 2, 'l'); - StringUTF16.putChar(buf, index + 3, 's'); - StringUTF16.putChar(buf, index + 4, 'e'); - } - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value char value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, char value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - buf[--index] = (byte) (value & 0xFF); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - StringUTF16.putChar(buf, --index, value); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value int value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, int value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - index = DecimalDigits.uncheckedGetCharsLatin1(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - index = DecimalDigits.uncheckedGetCharsUTF16(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value long value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, long value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - index = DecimalDigits.uncheckedGetCharsLatin1(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - index = DecimalDigits.uncheckedGetCharsUTF16(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value boolean value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, String value, String prefix) { - int index = ((int)indexCoder) - value.length(); - if (indexCoder < UTF16) { - value.getBytes(buf, index, String.LATIN1); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - value.getBytes(buf, index, String.UTF16); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Instantiates the String with given buffer and coder - * @param buf buffer to use - * @param indexCoder remaining index (should be zero) and coder - * @return String resulting string - */ - static String newString(byte[] buf, long indexCoder) { - // Use the private, non-copying constructor (unsafe!) - if (indexCoder == LATIN1) { - return new String(buf, String.LATIN1); - } else if (indexCoder == UTF16) { - return new String(buf, String.UTF16); - } else { - throw new InternalError("Storage is not completely initialized, " + - (int)indexCoder + " bytes left"); - } - } - /** * Perform a simple concatenation between two objects. Added for startup * performance, but also demonstrates the code that would be emitted by @@ -466,10 +210,6 @@ final class StringConcatHelper { return (value == null || (s = value.toString()) == null) ? "null" : s; } - private static final long LATIN1 = (long)String.LATIN1 << 32; - - private static final long UTF16 = (long)String.UTF16 << 32; - private static final Unsafe UNSAFE = Unsafe.getUnsafe(); static String stringOf(float value) { @@ -530,41 +270,6 @@ final class StringConcatHelper { return checkOverflow(length + value.length()); } - /** - * Allocates an uninitialized byte array based on the length and coder - * information, then prepends the given suffix string at the end of the - * byte array before returning it. The calling code must adjust the - * indexCoder so that it's taken the coder of the suffix into account, but - * subtracted the length of the suffix. - * - * @param suffix - * @param indexCoder - * @return the newly allocated byte array - */ - @ForceInline - static byte[] newArrayWithSuffix(String suffix, long indexCoder) { - byte[] buf = newArray(indexCoder + suffix.length()); - if (indexCoder < UTF16) { - suffix.getBytes(buf, (int)indexCoder, String.LATIN1); - } else { - suffix.getBytes(buf, (int)indexCoder, String.UTF16); - } - return buf; - } - - /** - * Allocates an uninitialized byte array based on the length and coder information - * in indexCoder - * @param indexCoder - * @return the newly allocated byte array - */ - @ForceInline - static byte[] newArray(long indexCoder) { - byte coder = (byte)(indexCoder >> 32); - int index = ((int)indexCoder) << coder; - return newArray(index); - } - /** * Allocates an uninitialized byte array based on the length * @param length @@ -578,14 +283,6 @@ final class StringConcatHelper { return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length); } - /** - * Provides the initial coder for the String. - * @return initial coder, adjusted into the upper half - */ - static long initialCoder() { - return String.COMPACT_STRINGS ? LATIN1 : UTF16; - } - static MethodHandle lookupStatic(String name, MethodType methodType) { try { return MethodHandles.lookup() @@ -603,7 +300,8 @@ final class StringConcatHelper { * subtracted the length of the suffix. * * @param suffix - * @param indexCoder + * @param index final char index in the buffer + * @param coder coder of the buffer * @return the newly allocated byte array */ @ForceInline diff --git a/src/java.base/share/classes/java/lang/StringLatin1.java b/src/java.base/share/classes/java/lang/StringLatin1.java index da3acbe5f0a..61c62d049bc 100644 --- a/src/java.base/share/classes/java/lang/StringLatin1.java +++ b/src/java.base/share/classes/java/lang/StringLatin1.java @@ -41,61 +41,49 @@ import static java.lang.String.checkIndex; import static java.lang.String.checkOffset; final class StringLatin1 { - public static char charAt(byte[] value, int index) { + static char charAt(byte[] value, int index) { checkIndex(index, value.length); return (char)(value[index] & 0xff); } - public static boolean canEncode(char cp) { + static boolean canEncode(char cp) { return cp <= 0xff; } - public static boolean canEncode(int cp) { + static boolean canEncode(int cp) { return cp >=0 && cp <= 0xff; } - public static byte coderFromChar(char cp) { + static byte coderFromChar(char cp) { return (byte)((0xff - cp) >>> (Integer.SIZE - 1)); } - public static int length(byte[] value) { + static int length(byte[] value) { return value.length; } - public static int codePointAt(byte[] value, int index, int end) { - return value[index] & 0xff; - } - - public static int codePointBefore(byte[] value, int index) { - return value[index - 1] & 0xff; - } - - public static int codePointCount(byte[] value, int beginIndex, int endIndex) { - return endIndex - beginIndex; - } - - public static char[] toChars(byte[] value) { + static char[] toChars(byte[] value) { char[] dst = new char[value.length]; inflate(value, 0, dst, 0, value.length); return dst; } - public static byte[] inflate(byte[] value, int off, int len) { + static byte[] inflate(byte[] value, int off, int len) { byte[] ret = StringUTF16.newBytesFor(len); inflate(value, off, ret, 0, len); return ret; } - public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { + static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { inflate(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } - public static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { + static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } @IntrinsicCandidate - public static boolean equals(byte[] value, byte[] other) { + static boolean equals(byte[] value, byte[] other) { if (value.length == other.length) { for (int i = 0; i < value.length; i++) { if (value[i] != other[i]) { @@ -108,20 +96,20 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int compareTo(byte[] value, byte[] other) { + static int compareTo(byte[] value, byte[] other) { int len1 = value.length; int len2 = other.length; return compareTo(value, other, len1, len2); } - public static int compareTo(byte[] value, byte[] other, int len1, int len2) { + static int compareTo(byte[] value, byte[] other, int len1, int len2) { int lim = Math.min(len1, len2); int k = ArraysSupport.mismatch(value, other, lim); return (k < 0) ? len1 - len2 : getChar(value, k) - getChar(other, k); } @IntrinsicCandidate - public static int compareToUTF16(byte[] value, byte[] other) { + static int compareToUTF16(byte[] value, byte[] other) { int len1 = length(value); int len2 = StringUTF16.length(other); return compareToUTF16Values(value, other, len1, len2); @@ -130,7 +118,7 @@ final class StringLatin1 { /* * Checks the boundary and then compares the byte arrays. */ - public static int compareToUTF16(byte[] value, byte[] other, int len1, int len2) { + static int compareToUTF16(byte[] value, byte[] other, int len1, int len2) { checkOffset(len1, length(value)); checkOffset(len2, StringUTF16.length(other)); @@ -149,7 +137,7 @@ final class StringLatin1 { return len1 - len2; } - public static int compareToCI(byte[] value, byte[] other) { + static int compareToCI(byte[] value, byte[] other) { int len1 = value.length; int len2 = other.length; int lim = Math.min(len1, len2); @@ -169,7 +157,7 @@ final class StringLatin1 { return len1 - len2; } - public static int compareToCI_UTF16(byte[] value, byte[] other) { + static int compareToCI_UTF16(byte[] value, byte[] other) { int len1 = length(value); int len2 = StringUTF16.length(other); int lim = Math.min(len1, len2); @@ -191,12 +179,12 @@ final class StringLatin1 { return len1 - len2; } - public static int hashCode(byte[] value) { + static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUnsigned(value, 0, value.length, 0); } // Caller must ensure that from- and toIndex are within bounds - public static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { + static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { if (!canEncode(ch)) { return -1; } @@ -215,7 +203,7 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int indexOf(byte[] value, byte[] str) { + static int indexOf(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -226,7 +214,7 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { + static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { byte first = str[0]; int max = (valueCount - strCount); for (int i = fromIndex; i <= max; i++) { @@ -248,8 +236,8 @@ final class StringLatin1 { return -1; } - public static int lastIndexOf(byte[] src, int srcCount, - byte[] tgt, int tgtCount, int fromIndex) { + static int lastIndexOf(byte[] src, int srcCount, + byte[] tgt, int tgtCount, int fromIndex) { int min = tgtCount - 1; int i = min + fromIndex; int strLastIndex = tgtCount - 1; @@ -276,7 +264,7 @@ final class StringLatin1 { } } - public static int lastIndexOf(final byte[] value, int ch, int fromIndex) { + static int lastIndexOf(final byte[] value, int ch, int fromIndex) { if (!canEncode(ch)) { return -1; } @@ -289,7 +277,7 @@ final class StringLatin1 { return -1; } - public static String replace(byte[] value, char oldChar, char newChar) { + static String replace(byte[] value, char oldChar, char newChar) { if (canEncode(oldChar)) { int len = value.length; int i = -1; @@ -326,8 +314,8 @@ final class StringLatin1 { return null; // for string to return this; } - public static String replace(byte[] value, int valLen, byte[] targ, - int targLen, byte[] repl, int replLen) + static String replace(byte[] value, int valLen, byte[] targ, + int targLen, byte[] repl, int replLen) { assert targLen > 0; int i, j, p = 0; @@ -377,8 +365,8 @@ final class StringLatin1 { } // case insensitive - public static boolean regionMatchesCI(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI(byte[] value, int toffset, + byte[] other, int ooffset, int len) { int last = toffset + len; while (toffset < last) { byte b1 = value[toffset++]; @@ -391,8 +379,8 @@ final class StringLatin1 { return true; } - public static boolean regionMatchesCI_UTF16(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI_UTF16(byte[] value, int toffset, + byte[] other, int ooffset, int len) { int last = toffset + len; while (toffset < last) { char c1 = (char)(value[toffset++] & 0xff); @@ -413,7 +401,7 @@ final class StringLatin1 { return true; } - public static String toLowerCase(String str, byte[] value, Locale locale) { + static String toLowerCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -480,7 +468,7 @@ final class StringLatin1 { return StringUTF16.newString(result, 0, resultOffset); } - public static String toUpperCase(String str, byte[] value, Locale locale) { + static String toUpperCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -560,7 +548,7 @@ final class StringLatin1 { return StringUTF16.newString(result, 0, resultOffset); } - public static String trim(byte[] value) { + static String trim(byte[] value) { int len = value.length; int st = 0; while ((st < len) && ((value[st] & 0xff) <= ' ')) { @@ -573,7 +561,7 @@ final class StringLatin1 { newString(value, st, len - st) : null; } - public static int indexOfNonWhitespace(byte[] value) { + static int indexOfNonWhitespace(byte[] value) { int length = value.length; int left = 0; while (left < length) { @@ -586,9 +574,8 @@ final class StringLatin1 { return left; } - public static int lastIndexOfNonWhitespace(byte[] value) { - int length = value.length; - int right = length; + static int lastIndexOfNonWhitespace(byte[] value) { + int right = value.length; while (0 < right) { char ch = getChar(value, right - 1); if (ch != ' ' && ch != '\t' && !CharacterDataLatin1.instance.isWhitespace(ch)) { @@ -599,7 +586,7 @@ final class StringLatin1 { return right; } - public static String strip(byte[] value) { + static String strip(byte[] value) { int left = indexOfNonWhitespace(value); if (left == value.length) { return ""; @@ -609,12 +596,12 @@ final class StringLatin1 { return ifChanged ? newString(value, left, right - left) : null; } - public static String stripLeading(byte[] value) { + static String stripLeading(byte[] value) { int left = indexOfNonWhitespace(value); return (left != 0) ? newString(value, left, value.length - left) : null; } - public static String stripTrailing(byte[] value) { + static String stripTrailing(byte[] value) { int right = lastIndexOfNonWhitespace(value); return (right != value.length) ? newString(value, 0, right) : null; } @@ -713,14 +700,14 @@ final class StringLatin1 { return StreamSupport.stream(LinesSpliterator.spliterator(value), false); } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { value[i] = (byte)c1; value[i + 1] = (byte)c2; value[i + 2] = (byte)c3; value[i + 3] = (byte)c4; } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { value[i] = (byte)c1; value[i + 1] = (byte)c2; value[i + 2] = (byte)c3; @@ -728,32 +715,15 @@ final class StringLatin1 { value[i + 4] = (byte)c5; } - public static void putChar(byte[] val, int index, int c) { - //assert (canEncode(c)); - val[index] = (byte)(c); - } - - public static char getChar(byte[] val, int index) { + static char getChar(byte[] val, int index) { return (char)(val[index] & 0xff); } - public static byte[] toBytes(int[] val, int off, int len) { - byte[] ret = new byte[len]; - for (int i = 0; i < len; i++) { - int cp = val[off++]; - if (!canEncode(cp)) { - return null; - } - ret[i] = (byte)cp; - } - return ret; - } - - public static byte[] toBytes(char c) { + static byte[] toBytes(char c) { return new byte[] { (byte)c }; } - public static String newString(byte[] val, int index, int len) { + static String newString(byte[] val, int index, int len) { if (len == 0) { return ""; } @@ -763,7 +733,7 @@ final class StringLatin1 { // inflatedCopy byte[] -> char[] @IntrinsicCandidate - public static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { dst[dstOff++] = (char)(src[srcOff++] & 0xff); } @@ -771,7 +741,7 @@ final class StringLatin1 { // inflatedCopy byte[] -> byte[] @IntrinsicCandidate - public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { StringUTF16.inflate(src, srcOff, dst, dstOff, len); } @@ -824,7 +794,7 @@ final class StringLatin1 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { diff --git a/src/java.base/share/classes/java/lang/StringUTF16.java b/src/java.base/share/classes/java/lang/StringUTF16.java index 08b4072fc35..4e31c9728e9 100644 --- a/src/java.base/share/classes/java/lang/StringUTF16.java +++ b/src/java.base/share/classes/java/lang/StringUTF16.java @@ -54,13 +54,13 @@ final class StringUTF16 { // Return a new byte array for a UTF16-coded string for len chars // Throw an exception if out of range - public static byte[] newBytesFor(int len) { + static byte[] newBytesFor(int len) { return new byte[newBytesLength(len)]; } // Check the size of a UTF16-coded string // Throw an exception if out of range - public static int newBytesLength(int len) { + static int newBytesLength(int len) { if (len < 0) { throw new NegativeArraySizeException(); } @@ -89,7 +89,7 @@ final class StringUTF16 { ((val[index] & 0xff) << LO_BYTE_SHIFT)); } - public static int length(byte[] value) { + static int length(byte[] value) { return value.length >> 1; } @@ -111,7 +111,7 @@ final class StringUTF16 { return c1; } - public static int codePointAt(byte[] value, int index, int end) { + static int codePointAt(byte[] value, int index, int end) { return codePointAt(value, index, end, false /* unchecked */); } @@ -134,7 +134,7 @@ final class StringUTF16 { return c2; } - public static int codePointBefore(byte[] value, int index) { + static int codePointBefore(byte[] value, int index) { return codePointBefore(value, index, false /* unchecked */); } @@ -155,11 +155,11 @@ final class StringUTF16 { return count; } - public static int codePointCount(byte[] value, int beginIndex, int endIndex) { + static int codePointCount(byte[] value, int beginIndex, int endIndex) { return codePointCount(value, beginIndex, endIndex, false /* unchecked */); } - public static char[] toChars(byte[] value) { + static char[] toChars(byte[] value) { char[] dst = new char[value.length >> 1]; getChars(value, 0, dst.length, dst, 0); return dst; @@ -173,7 +173,7 @@ final class StringUTF16 { * @param len a length */ @IntrinsicCandidate - public static byte[] toBytes(char[] value, int off, int len) { + static byte[] toBytes(char[] value, int off, int len) { byte[] val = newBytesFor(len); for (int i = 0; i < len; i++) { putChar(val, i, value[off]); @@ -218,7 +218,7 @@ final class StringUTF16 { * @param count count of chars to be compressed, {@code count} > 0 */ @ForceInline - public static byte[] compress(final char[] val, final int off, final int count) { + static byte[] compress(final char[] val, final int off, final int count) { byte[] latin1 = new byte[count]; int ndx = compress(val, off, latin1, 0, count); if (ndx != count) { @@ -245,7 +245,7 @@ final class StringUTF16 { * @param off starting offset * @param count count of chars to be compressed, {@code count} > 0 */ - public static byte[] compress(final byte[] val, final int off, final int count) { + static byte[] compress(final byte[] val, final int off, final int count) { byte[] latin1 = new byte[count]; int ndx = compress(val, off, latin1, 0, count); if (ndx != count) {// Switch to UTF16 @@ -279,7 +279,7 @@ final class StringUTF16 { * @param off starting offset * @param count length of code points to be compressed, length > 0 */ - public static byte[] compress(final int[] val, int off, final int count) { + static byte[] compress(final int[] val, int off, final int count) { // Optimistically copy all latin1 code points to the destination byte[] latin1 = new byte[count]; final int end = off + count; @@ -389,7 +389,7 @@ final class StringUTF16 { // compressedCopy char[] -> byte[] @IntrinsicCandidate - public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { + static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { char c = src[srcOff]; if (c > 0xff) { @@ -404,7 +404,7 @@ final class StringUTF16 { // compressedCopy byte[] -> byte[] @IntrinsicCandidate - public static int compress(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static int compress(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'getChar' has no checks checkBoundsOffCount(srcOff, len, src); for (int i = 0; i < len; i++) { @@ -420,7 +420,7 @@ final class StringUTF16 { } // Create the UTF16 buffer for !COMPACT_STRINGS - public static byte[] toBytes(int[] val, int index, int len) { + static byte[] toBytes(int[] val, int index, int len) { final int end = index + len; int n = computeCodePointSize(val, index, end); @@ -428,7 +428,7 @@ final class StringUTF16 { return extractCodepoints(val, index, end, buf, 0); } - public static byte[] toBytes(char c) { + static byte[] toBytes(char c) { byte[] result = new byte[2]; putChar(result, 0, c); return result; @@ -442,7 +442,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { + static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { // We need a range check here because 'getChar' has no checks if (srcBegin < srcEnd) { checkBoundsOffCount(srcBegin, srcEnd - srcBegin, value); @@ -453,7 +453,7 @@ final class StringUTF16 { } /* @see java.lang.String.getBytes(int, int, byte[], int) */ - public static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { + static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { srcBegin <<= 1; srcEnd <<= 1; for (int i = srcBegin + (1 >> LO_BYTE_SHIFT); i < srcEnd; i += 2) { @@ -462,7 +462,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int compareTo(byte[] value, byte[] other) { + static int compareTo(byte[] value, byte[] other) { int len1 = length(value); int len2 = length(other); return compareValues(value, other, len1, len2); @@ -471,7 +471,7 @@ final class StringUTF16 { /* * Checks the boundary and then compares the byte arrays. */ - public static int compareTo(byte[] value, byte[] other, int len1, int len2) { + static int compareTo(byte[] value, byte[] other, int len1, int len2) { checkOffset(len1, value); checkOffset(len2, other); @@ -491,15 +491,15 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int compareToLatin1(byte[] value, byte[] other) { + static int compareToLatin1(byte[] value, byte[] other) { return -StringLatin1.compareToUTF16(other, value); } - public static int compareToLatin1(byte[] value, byte[] other, int len1, int len2) { + static int compareToLatin1(byte[] value, byte[] other, int len1, int len2) { return -StringLatin1.compareToUTF16(other, value, len2, len1); } - public static int compareToCI(byte[] value, byte[] other) { + static int compareToCI(byte[] value, byte[] other) { return compareToCIImpl(value, 0, length(value), other, 0, length(other)); } @@ -512,8 +512,8 @@ final class StringUTF16 { assert olast <= length(other); for (int k1 = toffset, k2 = ooffset; k1 < tlast && k2 < olast; k1++, k2++) { - int cp1 = (int)getChar(value, k1); - int cp2 = (int)getChar(other, k2); + int cp1 = getChar(value, k1); + int cp2 = getChar(other, k2); if (cp1 == cp2 || compareCodePointCI(cp1, cp2) == 0) { continue; @@ -588,16 +588,16 @@ final class StringUTF16 { return cp; } - public static int compareToCI_Latin1(byte[] value, byte[] other) { + static int compareToCI_Latin1(byte[] value, byte[] other) { return -StringLatin1.compareToCI_UTF16(other, value); } - public static int hashCode(byte[] value) { + static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUTF16(value, 0, value.length >> 1, 0); } // Caller must ensure that from- and toIndex are within bounds - public static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { + static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) @@ -608,7 +608,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOf(byte[] value, byte[] str) { + static int indexOf(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -619,7 +619,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { + static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { checkBoundsBeginEnd(fromIndex, valueCount, value); checkBoundsBeginEnd(0, strCount, str); return indexOfUnsafe(value, valueCount, str, strCount, fromIndex); @@ -657,7 +657,7 @@ final class StringUTF16 { * Handles indexOf Latin1 substring in UTF16 string. */ @IntrinsicCandidate - public static int indexOfLatin1(byte[] value, byte[] str) { + static int indexOfLatin1(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -668,13 +668,13 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { + static int indexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { checkBoundsBeginEnd(fromIndex, srcCount, src); String.checkBoundsBeginEnd(0, tgtCount, tgt.length); return indexOfLatin1Unsafe(src, srcCount, tgt, tgtCount, fromIndex); } - public static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { + static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; assert tgtCount <= tgt.length; @@ -730,8 +730,8 @@ final class StringUTF16 { } // srcCoder == UTF16 && tgtCoder == UTF16 - public static int lastIndexOf(byte[] src, int srcCount, - byte[] tgt, int tgtCount, int fromIndex) { + static int lastIndexOf(byte[] src, int srcCount, + byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; assert tgtCount <= length(tgt); @@ -765,7 +765,7 @@ final class StringUTF16 { } } - public static int lastIndexOf(byte[] value, int ch, int fromIndex) { + static int lastIndexOf(byte[] value, int ch, int fromIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) @@ -798,7 +798,7 @@ final class StringUTF16 { return -1; } - public static String replace(byte[] value, char oldChar, char newChar) { + static String replace(byte[] value, char oldChar, char newChar) { int len = value.length >> 1; int i = -1; while (++i < len) { @@ -829,9 +829,9 @@ final class StringUTF16 { return null; } - public static String replace(byte[] value, int valLen, boolean valLat1, - byte[] targ, int targLen, boolean targLat1, - byte[] repl, int replLen, boolean replLat1) + static String replace(byte[] value, int valLen, boolean valLat1, + byte[] targ, int targLen, boolean targLat1, + byte[] repl, int replLen, boolean replLat1) { assert targLen > 0; assert !valLat1 || !targLat1 || !replLat1; @@ -944,18 +944,18 @@ final class StringUTF16 { return new String(result, UTF16); } - public static boolean regionMatchesCI(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI(byte[] value, int toffset, + byte[] other, int ooffset, int len) { return compareToCIImpl(value, toffset, len, other, ooffset, len) == 0; } - public static boolean regionMatchesCI_Latin1(byte[] value, int toffset, - byte[] other, int ooffset, - int len) { + static boolean regionMatchesCI_Latin1(byte[] value, int toffset, + byte[] other, int ooffset, + int len) { return StringLatin1.regionMatchesCI_UTF16(other, ooffset, value, toffset, len); } - public static String toLowerCase(String str, byte[] value, Locale locale) { + static String toLowerCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -965,7 +965,7 @@ final class StringUTF16 { // Now check if there are any characters that need to be changed, or are surrogate for (first = 0 ; first < len; first++) { - int cp = (int)getChar(value, first); + int cp = getChar(value, first); if (Character.isSurrogate((char)cp)) { hasSurr = true; break; @@ -988,7 +988,7 @@ final class StringUTF16 { } int bits = 0; for (int i = first; i < len; i++) { - int cp = (int)getChar(value, i); + int cp = getChar(value, i); if (cp == '\u03A3' || // GREEK CAPITAL LETTER SIGMA Character.isSurrogate((char)cp)) { return toLowerCaseEx(str, value, result, i, locale, false); @@ -1003,7 +1003,7 @@ final class StringUTF16 { bits |= cp; putChar(result, i, cp); } - if (bits < 0 || bits > 0xff) { + if (bits > 0xff) { return new String(result, UTF16); } else { return newString(result, 0, len); @@ -1059,7 +1059,7 @@ final class StringUTF16 { return newString(result, 0, resultOffset); } - public static String toUpperCase(String str, byte[] value, Locale locale) { + static String toUpperCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -1069,7 +1069,7 @@ final class StringUTF16 { // Now check if there are any characters that need to be changed, or are surrogate for (first = 0 ; first < len; first++) { - int cp = (int)getChar(value, first); + int cp = getChar(value, first); if (Character.isSurrogate((char)cp)) { hasSurr = true; break; @@ -1093,7 +1093,7 @@ final class StringUTF16 { } int bits = 0; for (int i = first; i < len; i++) { - int cp = (int)getChar(value, i); + int cp = getChar(value, i); if (Character.isSurrogate((char)cp)) { return toUpperCaseEx(str, value, result, i, locale, false); } @@ -1104,7 +1104,7 @@ final class StringUTF16 { bits |= cp; putChar(result, i, cp); } - if (bits < 0 || bits > 0xff) { + if (bits > 0xff) { return new String(result, UTF16); } else { return newString(result, 0, len); @@ -1164,7 +1164,7 @@ final class StringUTF16 { return newString(result, 0, resultOffset); } - public static String trim(byte[] value) { + static String trim(byte[] value) { int length = value.length >> 1; int len = length; int st = 0; @@ -1179,7 +1179,7 @@ final class StringUTF16 { null; } - public static int indexOfNonWhitespace(byte[] value) { + static int indexOfNonWhitespace(byte[] value) { int length = value.length >> 1; int left = 0; while (left < length) { @@ -1192,9 +1192,8 @@ final class StringUTF16 { return left; } - public static int lastIndexOfNonWhitespace(byte[] value) { - int length = value.length >>> 1; - int right = length; + static int lastIndexOfNonWhitespace(byte[] value) { + int right = value.length >>> 1; while (0 < right) { int codepoint = codePointBefore(value, right); if (codepoint != ' ' && codepoint != '\t' && !Character.isWhitespace(codepoint)) { @@ -1205,7 +1204,7 @@ final class StringUTF16 { return right; } - public static String strip(byte[] value) { + static String strip(byte[] value) { int length = value.length >>> 1; int left = indexOfNonWhitespace(value); if (left == length) { @@ -1216,13 +1215,13 @@ final class StringUTF16 { return ifChanged ? newString(value, left, right - left) : null; } - public static String stripLeading(byte[] value) { + static String stripLeading(byte[] value) { int length = value.length >>> 1; int left = indexOfNonWhitespace(value); return (left != 0) ? newString(value, left, length - left) : null; } - public static String stripTrailing(byte[] value) { + static String stripTrailing(byte[] value) { int length = value.length >>> 1; int right = lastIndexOfNonWhitespace(value); return (right != length) ? newString(value, 0, right) : null; @@ -1322,7 +1321,7 @@ final class StringUTF16 { return StreamSupport.stream(LinesSpliterator.spliterator(value), false); } - public static String newString(byte[] val, int index, int len) { + static String newString(byte[] val, int index, int len) { if (len == 0) { return ""; } @@ -1388,7 +1387,7 @@ final class StringUTF16 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { @@ -1473,7 +1472,7 @@ final class StringUTF16 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { @@ -1483,12 +1482,12 @@ final class StringUTF16 { //////////////////////////////////////////////////////////////// - public static void putCharSB(byte[] val, int index, int c) { + static void putCharSB(byte[] val, int index, int c) { checkIndex(index, val); putChar(val, index, c); } - public static void putCharsSB(byte[] val, int index, char[] ca, int off, int end) { + static void putCharsSB(byte[] val, int index, char[] ca, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); String.checkBoundsBeginEnd(off, end, ca.length); Unsafe.getUnsafe().copyMemory( @@ -1499,26 +1498,26 @@ final class StringUTF16 { (long) (end - off) << 1); } - public static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { + static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); for (int i = off; i < end; i++) { putChar(val, index++, s.charAt(i)); } } - public static int codePointAtSB(byte[] val, int index, int end) { + static int codePointAtSB(byte[] val, int index, int end) { return codePointAt(val, index, end, true /* checked */); } - public static int codePointBeforeSB(byte[] val, int index) { + static int codePointBeforeSB(byte[] val, int index) { return codePointBefore(val, index, true /* checked */); } - public static int codePointCountSB(byte[] val, int beginIndex, int endIndex) { + static int codePointCountSB(byte[] val, int beginIndex, int endIndex) { return codePointCount(val, beginIndex, endIndex, true /* checked */); } - public static boolean contentEquals(byte[] v1, byte[] v2, int len) { + static boolean contentEquals(byte[] v1, byte[] v2, int len) { checkBoundsOffCount(0, len, v2); for (int i = 0; i < len; i++) { if ((char)(v1[i] & 0xff) != getChar(v2, i)) { @@ -1528,7 +1527,7 @@ final class StringUTF16 { return true; } - public static boolean contentEquals(byte[] value, CharSequence cs, int len) { + static boolean contentEquals(byte[] value, CharSequence cs, int len) { checkOffset(len, value); for (int i = 0; i < len; i++) { if (getChar(value, i) != cs.charAt(i)) { @@ -1538,7 +1537,7 @@ final class StringUTF16 { return true; } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { int end = i + 4; checkBoundsBeginEnd(i, end, value); putChar(value, i, c1); @@ -1547,7 +1546,7 @@ final class StringUTF16 { putChar(value, i + 3, c4); } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { int end = i + 5; checkBoundsBeginEnd(i, end, value); putChar(value, i, c1); @@ -1557,12 +1556,12 @@ final class StringUTF16 { putChar(value, i + 4, c5); } - public static char charAt(byte[] value, int index) { + static char charAt(byte[] value, int index) { checkIndex(index, value); return getChar(value, index); } - public static void reverse(byte[] val, int count) { + static void reverse(byte[] val, int count) { checkOffset(count, val); int n = count - 1; boolean hasSurrogates = false; @@ -1597,7 +1596,7 @@ final class StringUTF16 { } // inflatedCopy byte[] -> byte[] - public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'putChar' has no checks checkBoundsOffCount(dstOff, len, dst); for (int i = 0; i < len; i++) { @@ -1606,7 +1605,7 @@ final class StringUTF16 { } // srcCoder == UTF16 && tgtCoder == LATIN1 - public static int lastIndexOfLatin1(byte[] src, int srcCount, + static int lastIndexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; @@ -1659,19 +1658,19 @@ final class StringUTF16 { static final int MAX_LENGTH = Integer.MAX_VALUE >> 1; - public static void checkIndex(int off, byte[] val) { + static void checkIndex(int off, byte[] val) { String.checkIndex(off, length(val)); } - public static void checkOffset(int off, byte[] val) { + static void checkOffset(int off, byte[] val) { String.checkOffset(off, length(val)); } - public static void checkBoundsBeginEnd(int begin, int end, byte[] val) { + static void checkBoundsBeginEnd(int begin, int end, byte[] val) { String.checkBoundsBeginEnd(begin, end, length(val)); } - public static void checkBoundsOffCount(int offset, int count, byte[] val) { + static void checkBoundsOffCount(int offset, int count, byte[] val) { String.checkBoundsOffCount(offset, count, length(val)); } diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index bb1775fbc6b..c88cf4ac797 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -2185,18 +2185,6 @@ public final class System { return StringConcatHelper.lookupStatic(name, methodType); } - public long stringConcatInitialCoder() { - return StringConcatHelper.initialCoder(); - } - - public long stringConcatMix(long lengthCoder, String constant) { - return StringConcatHelper.mix(lengthCoder, constant); - } - - public long stringConcatMix(long lengthCoder, char value) { - return StringConcatHelper.mix(lengthCoder, value); - } - public Object uncheckedStringConcat1(String[] constants) { return new StringConcatHelper.Concat1(constants); } diff --git a/src/java.base/share/classes/java/lang/Thread.java b/src/java.base/share/classes/java/lang/Thread.java index 2fc20a94f1d..40f2dacadd2 100644 --- a/src/java.base/share/classes/java/lang/Thread.java +++ b/src/java.base/share/classes/java/lang/Thread.java @@ -1527,36 +1527,6 @@ public class Thread implements Runnable { } } - /** - * Throws {@code UnsupportedOperationException}. - * - * @throws UnsupportedOperationException always - * - * @deprecated This method was originally specified to "stop" a victim - * thread by causing the victim thread to throw a {@link ThreadDeath}. - * It was inherently unsafe. Stopping a thread caused it to unlock - * all of the monitors that it had locked (as a natural consequence - * of the {@code ThreadDeath} exception propagating up the stack). If - * any of the objects previously protected by these monitors were in - * an inconsistent state, the damaged objects became visible to - * other threads, potentially resulting in arbitrary behavior. - * Usages of {@code stop} should be replaced by code that simply - * modifies some variable to indicate that the target thread should - * stop running. The target thread should check this variable - * regularly, and return from its run method in an orderly fashion - * if the variable indicates that it is to stop running. If the - * target thread waits for long periods (on a condition variable, - * for example), the {@code interrupt} method should be used to - * interrupt the wait. - * For more information, see - * Why - * is Thread.stop deprecated and the ability to stop a thread removed?. - */ - @Deprecated(since="1.2", forRemoval=true) - public final void stop() { - throw new UnsupportedOperationException(); - } - /** * Interrupts this thread. * diff --git a/src/java.base/share/classes/java/lang/ThreadDeath.java b/src/java.base/share/classes/java/lang/ThreadDeath.java index 743f1178abe..e77029a28fc 100644 --- a/src/java.base/share/classes/java/lang/ThreadDeath.java +++ b/src/java.base/share/classes/java/lang/ThreadDeath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,14 +27,12 @@ package java.lang; /** * An instance of {@code ThreadDeath} was originally specified to be thrown - * by a victim thread when "stopped" with {@link Thread#stop()}. + * by a victim thread when "stopped" with the {@link Thread} API. * - * @deprecated {@link Thread#stop()} was originally specified to "stop" a victim - * thread by causing the victim thread to throw a {@code ThreadDeath}. It - * was inherently unsafe and deprecated in an early JDK release. The ability - * to "stop" a thread with {@code Thread.stop} has been removed and the - * {@code Thread.stop} method changed to throw an exception. Consequently, - * {@code ThreadDeath} is also deprecated, for removal. + * @deprecated {@code Thread} originally specified a "{@code stop}" method to stop a + * victim thread by causing the victim thread to throw a {@code ThreadDeath}. It + * was inherently unsafe and deprecated in an early JDK release. The {@code stop} + * method has since been removed and {@code ThreadDeath} is deprecated, for removal. * * @since 1.0 */ diff --git a/src/java.base/share/classes/java/lang/classfile/MethodSignature.java b/src/java.base/share/classes/java/lang/classfile/MethodSignature.java index f81820a7f77..76251cd4d8e 100644 --- a/src/java.base/share/classes/java/lang/classfile/MethodSignature.java +++ b/src/java.base/share/classes/java/lang/classfile/MethodSignature.java @@ -113,13 +113,14 @@ public sealed interface MethodSignature * * @param result signature for the return type * @param arguments signatures for the method parameters + * @throws IllegalArgumentException if any of {@code arguments} is void */ public static MethodSignature of(Signature result, Signature... arguments) { return new SignaturesImpl.MethodSignatureImpl(List.of(), List.of(), requireNonNull(result), - List.of(arguments)); + SignaturesImpl.validateArgumentList(arguments)); } /** @@ -131,6 +132,7 @@ public sealed interface MethodSignature * @param exceptions signatures for the exceptions * @param result signature for the return type * @param arguments signatures for the method parameters + * @throws IllegalArgumentException if any of {@code arguments} is void */ public static MethodSignature of(List typeParameters, List exceptions, @@ -140,7 +142,7 @@ public sealed interface MethodSignature List.copyOf(requireNonNull(typeParameters)), List.copyOf(requireNonNull(exceptions)), requireNonNull(result), - List.of(arguments)); + SignaturesImpl.validateArgumentList(arguments)); } /** diff --git a/src/java.base/share/classes/java/lang/classfile/Signature.java b/src/java.base/share/classes/java/lang/classfile/Signature.java index f9af71083f9..594c7822e98 100644 --- a/src/java.base/share/classes/java/lang/classfile/Signature.java +++ b/src/java.base/share/classes/java/lang/classfile/Signature.java @@ -43,6 +43,11 @@ import static java.util.Objects.requireNonNull; /** * Models generic Java type signatures, as defined in JVMS {@jvms 4.7.9.1}. + *

    + * Names in signatures are identifiers, which must + * not be empty and must not contain any of the ASCII characters {@code + * . ; [ / < > :}. Top-level class and interface names are denoted by + * slash-separated identifiers. * * @see Type * @see SignatureAttribute @@ -73,6 +78,8 @@ public sealed interface Signature { * signature represents a reifiable type (JLS {@jls 4.7}). * * @param classDesc the symbolic description of the Java type + * @throws IllegalArgumentException if the field descriptor cannot be + * {@linkplain ##identifier denoted} */ public static Signature of(ClassDesc classDesc) { requireNonNull(classDesc); @@ -139,6 +146,31 @@ public sealed interface Signature { /** * Models the signature of a possibly-parameterized class or interface type. + *

    + * These are examples of class type signatures: + *

    + *

    + * If the {@linkplain #outerType() outer type} exists, the {@linkplain + * #className() class name} is the simple name of the nested type. + * Otherwise, it is a {@linkplain ClassEntry##internalname binary name in + * internal form} (separated by {@code /}). + *

    + * If a nested type does not have any enclosing parameterization, it may + * be represented without an outer type and as an internal binary name, + * in which nesting is represented by {@code $} instead of {@code .}. * * @see Type * @see ParameterizedType @@ -152,7 +184,8 @@ public sealed interface Signature { /** * {@return the signature of the class that this class is a member of, * only if this is a member class} Note that the outer class may be - * absent if it is not a parameterized type. + * absent if this is a member class without any parameterized enclosing + * type. * * @jls 4.5 Parameterized Types */ @@ -161,7 +194,8 @@ public sealed interface Signature { /** * {@return the class or interface name; includes the {@linkplain * ClassEntry##internalname slash-separated} package name if there is no - * outer type} + * outer type} Note this may indicate a nested class name with {@code $} + * separators if there is no parameterized enclosing type. */ String className(); @@ -188,10 +222,11 @@ public sealed interface Signature { * @param className the name of the class or interface * @param typeArgs the type arguments * @throws IllegalArgumentException if {@code className} does not - * represent a class or interface + * represent a class or interface, or if it cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(ClassDesc className, TypeArg... typeArgs) { - return of(null, className, typeArgs); + return of(null, Util.toInternalName(className), typeArgs); } /** @@ -201,8 +236,15 @@ public sealed interface Signature { * @param className the name of this class or interface * @param typeArgs the type arguments * @throws IllegalArgumentException if {@code className} does not - * represent a class or interface + * represent a class or interface, or if it cannot be + * {@linkplain Signature##identifier denoted} + * @deprecated + * The resulting signature does not denote the class represented by + * {@code className} when {@code outerType} is not null. Use {@link + * #of(ClassTypeSig, String, TypeArg...) of(ClassTypeSig, String, TypeArg...)} + * instead. */ + @Deprecated(since = "26", forRemoval = true) public static ClassTypeSig of(ClassTypeSig outerType, ClassDesc className, TypeArg... typeArgs) { requireNonNull(className); return of(outerType, Util.toInternalName(className), typeArgs); @@ -211,8 +253,11 @@ public sealed interface Signature { /** * {@return a class or interface signature without an outer type} * - * @param className the name of the class or interface + * @param className the name of the class or interface, may use + * {@code /} to separate * @param typeArgs the type arguments + * @throws IllegalArgumentException if {@code className} cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(String className, TypeArg... typeArgs) { return of(null, className, typeArgs); @@ -222,12 +267,19 @@ public sealed interface Signature { * {@return a class type signature} * * @param outerType signature of the outer type, may be {@code null} - * @param className the name of this class or interface + * @param className the name of this class or interface, may use + * {@code /} to separate if outer type is absent * @param typeArgs the type arguments + * @throws IllegalArgumentException if {@code className} cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(ClassTypeSig outerType, String className, TypeArg... typeArgs) { - requireNonNull(className); - return new SignaturesImpl.ClassTypeSigImpl(Optional.ofNullable(outerType), className.replace(".", "/"), List.of(typeArgs)); + if (outerType != null) { + SignaturesImpl.validateIdentifier(className); + } else { + SignaturesImpl.validatePackageSpecifierPlusIdentifier(className); + } + return new SignaturesImpl.ClassTypeSigImpl(Optional.ofNullable(outerType), className, List.of(typeArgs)); } } @@ -383,9 +435,11 @@ public sealed interface Signature { * {@return a signature for a type variable} * * @param identifier the name of the type variable + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeVarSig of(String identifier) { - return new SignaturesImpl.TypeVarSigImpl(requireNonNull(identifier)); + return new SignaturesImpl.TypeVarSigImpl(SignaturesImpl.validateIdentifier(identifier)); } } @@ -408,9 +462,10 @@ public sealed interface Signature { /** * {@return an array type with the given component type} * @param componentSignature the component type + * @throws IllegalArgumentException if the component type is void */ public static ArrayTypeSig of(Signature componentSignature) { - return of(1, requireNonNull(componentSignature)); + return of(1, SignaturesImpl.validateNonVoid(componentSignature)); } /** @@ -418,10 +473,11 @@ public sealed interface Signature { * @param dims the dimension of the array * @param componentSignature the component type * @throws IllegalArgumentException if {@code dims < 1} or the - * resulting array type exceeds 255 dimensions + * resulting array type exceeds 255 dimensions or the component + * type is void */ public static ArrayTypeSig of(int dims, Signature componentSignature) { - requireNonNull(componentSignature); + SignaturesImpl.validateNonVoid(componentSignature); if (componentSignature instanceof SignaturesImpl.ArrayTypeSigImpl arr) { if (dims < 1 || dims > 255 - arr.arrayDepth()) throw new IllegalArgumentException("illegal array depth value"); @@ -469,10 +525,12 @@ public sealed interface Signature { * @param identifier the name of the type parameter * @param classBound the class bound of the type parameter, may be {@code null} * @param interfaceBounds the interface bounds of the type parameter + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeParam of(String identifier, RefTypeSig classBound, RefTypeSig... interfaceBounds) { return new SignaturesImpl.TypeParamImpl( - requireNonNull(identifier), + SignaturesImpl.validateIdentifier(identifier), Optional.ofNullable(classBound), List.of(interfaceBounds)); } @@ -483,10 +541,12 @@ public sealed interface Signature { * @param identifier the name of the type parameter * @param classBound the optional class bound of the type parameter * @param interfaceBounds the interface bounds of the type parameter + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeParam of(String identifier, Optional classBound, RefTypeSig... interfaceBounds) { return new SignaturesImpl.TypeParamImpl( - requireNonNull(identifier), + SignaturesImpl.validateIdentifier(identifier), requireNonNull(classBound), List.of(interfaceBounds)); } diff --git a/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html b/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html deleted file mode 100644 index 9f3ad156727..00000000000 --- a/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - Java Thread Primitive Deprecation - - -

    Java Thread Primitive Deprecation

    -
    -

    Why is Thread.stop deprecated and the ability to -stop a thread removed?

    -

    Because it was inherently unsafe. Stopping a thread caused it to -unlock all the monitors that it had locked. (The monitors were -unlocked as the ThreadDeath exception propagated up -the stack.) If any of the objects previously protected by these -monitors were in an inconsistent state, other threads may have viewed -these objects in an inconsistent state. Such objects are said to be -damaged. When threads operate on damaged objects, arbitrary -behavior can result. This behavior may be subtle and difficult to -detect, or it may be pronounced. Unlike other unchecked exceptions, -ThreadDeath killed threads silently; thus, the user had -no warning that their program may be corrupted. The corruption could -manifest itself at any time after the actual damage occurs, even -hours or days in the future.

    -
    -

    Couldn't I have just caught ThreadDeath and fixed -the damaged object?

    -

    In theory, perhaps, but it would vastly complicate the -task of writing correct multithreaded code. The task would be -nearly insurmountable for two reasons:

    -
      -
    1. A thread could throw a ThreadDeath exception -almost anywhere. All synchronized methods and blocks would -have to be studied in great detail, with this in mind.
    2. -
    3. A thread could throw a second ThreadDeath exception -while cleaning up from the first (in the catch or -finally clause). Cleanup would have to be repeated till -it succeeded. The code to ensure this would be quite complex.
    4. -
    -In sum, it just isn't practical. -
    -

    What should I use instead of Thread.stop?

    -

    Most uses of stop should be replaced by code that -simply modifies some variable to indicate that the target thread -should stop running. The target thread should check this variable -regularly, and return from its run method in an orderly fashion if -the variable indicates that it is to stop running. To ensure prompt -communication of the stop-request, the variable must be -volatile (or access to the variable must be -synchronized).

    -

    For example, suppose your application contains the following -start, stop and run -methods:

    -
    -    private Thread blinker;
    -
    -    public void start() {
    -        blinker = new Thread(this);
    -        blinker.start();
    -    }
    -
    -    public void stop() {
    -        blinker.stop();  // UNSAFE!
    -    }
    -
    -    public void run() {
    -        while (true) {
    -            try {
    -                Thread.sleep(interval);
    -            } catch (InterruptedException e){
    -            }
    -            blink();
    -        }
    -    }
    -
    -You can avoid the use of Thread.stop by replacing the -application's stop and run methods with: -
    -    private volatile Thread blinker;
    -
    -    public void stop() {
    -        blinker = null;
    -    }
    -
    -    public void run() {
    -        Thread thisThread = Thread.currentThread();
    -        while (blinker == thisThread) {
    -            try {
    -                Thread.sleep(interval);
    -            } catch (InterruptedException e){
    -            }
    -            blink();
    -        }
    -    }
    -
    -
    -

    How do I stop a thread that waits for long periods (e.g., for -input)?

    -

    That's what the Thread.interrupt method is for. The -same "state based" signaling mechanism shown above can be used, but -the state change (blinker = null, in the previous -example) can be followed by a call to -Thread.interrupt, to interrupt the wait:

    -
    -    public void stop() {
    -        Thread moribund = waiter;
    -        waiter = null;
    -        moribund.interrupt();
    -    }
    -
    -For this technique to work, it's critical that any method that -catches an interrupt exception and is not prepared to deal with it -immediately reasserts the exception. We say reasserts -rather than rethrows, because it is not always possible to -rethrow the exception. If the method that catches the -InterruptedException is not declared to throw this -(checked) exception, then it should "reinterrupt itself" with the -following incantation: -
    -    Thread.currentThread().interrupt();
    -
    -This ensures that the Thread will reraise the -InterruptedException as soon as it is able. -
    -

    What if a thread doesn't respond to -Thread.interrupt?

    -

    In some cases, you can use application specific tricks. For -example, if a thread is waiting on a known socket, you can close -the socket to cause the thread to return immediately. -Unfortunately, there really isn't any technique that works in -general. It should be noted that in all situations where a -waiting thread doesn't respond to Thread.interrupt, it -wouldn't respond to Thread.stop either. Such -cases include deliberate denial-of-service attacks, and I/O -operations for which thread.stop and thread.interrupt do not work -properly.

    - - diff --git a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java index 1c7995c4ec7..733714b5786 100644 --- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java +++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java @@ -37,7 +37,6 @@ import jdk.internal.util.ReferenceKey; import jdk.internal.util.ReferencedKeyMap; import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.Stable; -import sun.invoke.util.Wrapper; import java.lang.classfile.Annotation; import java.lang.classfile.ClassBuilder; @@ -119,14 +118,10 @@ import static java.lang.invoke.MethodType.methodType; */ @AOTSafeClassInitializer public final class StringConcatFactory { - private static final int HIGH_ARITY_THRESHOLD; private static final int CACHE_THRESHOLD; private static final int FORCE_INLINE_THRESHOLD; static { - String highArity = VM.getSavedProperty("java.lang.invoke.StringConcat.highArityThreshold"); - HIGH_ARITY_THRESHOLD = highArity != null ? Integer.parseInt(highArity) : 0; - String cacheThreshold = VM.getSavedProperty("java.lang.invoke.StringConcat.cacheThreshold"); CACHE_THRESHOLD = cacheThreshold != null ? Integer.parseInt(cacheThreshold) : 256; @@ -391,9 +386,6 @@ public final class StringConcatFactory { try { MethodHandle mh = makeSimpleConcat(concatType, constantStrings); - if (mh == null && concatType.parameterCount() <= HIGH_ARITY_THRESHOLD) { - mh = generateMHInlineCopy(concatType, constantStrings); - } if (mh == null) { mh = InlineHiddenClassStrategy.generate(lookup, concatType, constantStrings); @@ -518,385 +510,6 @@ public final class StringConcatFactory { return null; } - /** - *

    This strategy replicates what StringBuilders are doing: it builds the - * byte[] array on its own and passes that byte[] array to String - * constructor. This strategy requires access to some private APIs in JDK, - * most notably, the private String constructor that accepts byte[] arrays - * without copying. - */ - private static MethodHandle generateMHInlineCopy(MethodType mt, String[] constants) { - int paramCount = mt.parameterCount(); - String suffix = constants[paramCount]; - - - // else... fall-through to slow-path - - // Create filters and obtain filtered parameter types. Filters would be used in the beginning - // to convert the incoming arguments into the arguments we can process (e.g. Objects -> Strings). - // The filtered argument type list is used all over in the combinators below. - - Class[] ptypes = mt.erase().parameterArray(); - MethodHandle[] objFilters = null; - MethodHandle[] floatFilters = null; - MethodHandle[] doubleFilters = null; - for (int i = 0; i < ptypes.length; i++) { - Class cl = ptypes[i]; - // Use int as the logical type for subword integral types - // (byte and short). char and boolean require special - // handling so don't change the logical type of those - ptypes[i] = promoteToIntType(ptypes[i]); - // Object, float and double will be eagerly transformed - // into a (non-null) String as a first step after invocation. - // Set up to use String as the logical type for such arguments - // internally. - if (cl == Object.class) { - if (objFilters == null) { - objFilters = new MethodHandle[ptypes.length]; - } - objFilters[i] = objectStringifier(); - ptypes[i] = String.class; - } else if (cl == float.class) { - if (floatFilters == null) { - floatFilters = new MethodHandle[ptypes.length]; - } - floatFilters[i] = floatStringifier(); - ptypes[i] = String.class; - } else if (cl == double.class) { - if (doubleFilters == null) { - doubleFilters = new MethodHandle[ptypes.length]; - } - doubleFilters[i] = doubleStringifier(); - ptypes[i] = String.class; - } - } - - // Start building the combinator tree. The tree "starts" with ()String, and "finishes" - // with the (byte[], long)String shape to invoke newString in StringConcatHelper. The combinators are - // assembled bottom-up, which makes the code arguably hard to read. - - // Drop all remaining parameter types, leave only helper arguments: - MethodHandle mh = MethodHandles.dropArgumentsTrusted(newString(), 2, ptypes); - - // Calculate the initialLengthCoder value by looking at all constant values and summing up - // their lengths and adjusting the encoded coder bit if needed - long initialLengthCoder = INITIAL_CODER; - - for (String constant : constants) { - if (constant != null) { - initialLengthCoder = JLA.stringConcatMix(initialLengthCoder, constant); - } - } - - // Mix in prependers. This happens when (byte[], long) = (storage, indexCoder) is already - // known from the combinators below. We are assembling the string backwards, so the index coded - // into indexCoder is the *ending* index. - mh = filterInPrependers(mh, constants, ptypes); - - // Fold in byte[] instantiation at argument 0 - MethodHandle newArrayCombinator; - if (suffix == null || suffix.isEmpty()) { - suffix = ""; - } - // newArray variant that deals with prepending any trailing constant - // - // initialLengthCoder is adjusted to have the correct coder - // and length: The newArrayWithSuffix method expects only the coder of the - // suffix to be encoded into indexCoder - initialLengthCoder -= suffix.length(); - newArrayCombinator = newArrayWithSuffix(suffix); - - mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArrayCombinator, - 1 // index - ); - - // Start combining length and coder mixers. - // - // Length is easy: constant lengths can be computed on the spot, and all non-constant - // shapes have been either converted to Strings, or explicit methods for getting the - // string length out of primitives are provided. - // - // Coders are more interesting. Only Object, String and char arguments (and constants) - // can have non-Latin1 encoding. It is easier to blindly convert constants to String, - // and deduce the coder from there. Arguments would be either converted to Strings - // during the initial filtering, or handled by specializations in MIXERS. - // - // The method handle shape before all mixers are combined in is: - // (long, )String = ("indexCoder", ) - // - // We will bind the initialLengthCoder value to the last mixer (the one that will be - // executed first), then fold that in. This leaves the shape after all mixers are - // combined in as: - // ()String = () - - mh = filterAndFoldInMixers(mh, initialLengthCoder, ptypes); - - // The method handle shape here is (). - - // Apply filters, converting the arguments: - if (objFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, objFilters); - } - if (floatFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, floatFilters); - } - if (doubleFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, doubleFilters); - } - - return mh; - } - - // We need one prepender per argument, but also need to fold in constants. We do so by greedily - // creating prependers that fold in surrounding constants into the argument prepender. This reduces - // the number of unique MH combinator tree shapes we'll create in an application. - // Additionally we do this in chunks to reduce the number of combinators bound to the root tree, - // which simplifies the shape and makes construction of similar trees use less unique LF classes - private static MethodHandle filterInPrependers(MethodHandle mh, String[] constants, Class[] ptypes) { - int pos; - int[] argPositions = null; - MethodHandle prepend; - for (pos = 0; pos < ptypes.length - 3; pos += 4) { - prepend = prepender(pos, constants, ptypes, 4); - argPositions = filterPrependArgPositions(argPositions, pos, 4); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepend, argPositions); - } - if (pos < ptypes.length) { - int count = ptypes.length - pos; - prepend = prepender(pos, constants, ptypes, count); - argPositions = filterPrependArgPositions(argPositions, pos, count); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepend, argPositions); - } - return mh; - } - - static int[] filterPrependArgPositions(int[] argPositions, int pos, int count) { - if (argPositions == null || argPositions.length != count + 2) { - argPositions = new int[count + 2]; - argPositions[0] = 1; // indexCoder - argPositions[1] = 0; // storage - } - int limit = count + 2; - for (int i = 2; i < limit; i++) { - argPositions[i] = i + pos; - } - return argPositions; - } - - - // We need one mixer per argument. - private static MethodHandle filterAndFoldInMixers(MethodHandle mh, long initialLengthCoder, Class[] ptypes) { - int pos; - int[] argPositions = null; - for (pos = 0; pos < ptypes.length - 4; pos += 4) { - // Compute new "index" in-place pairwise using old value plus the appropriate arguments. - MethodHandle mix = mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - argPositions = filterMixerArgPositions(argPositions, pos, 4); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 0, - mix, argPositions); - } - - if (pos < ptypes.length) { - // Mix in the last 1 to 4 parameters, insert the initialLengthCoder into the final mixer and - // fold the result into the main combinator - mh = foldInLastMixers(mh, initialLengthCoder, pos, ptypes, ptypes.length - pos); - } else if (ptypes.length == 0) { - // No mixer (constants only concat), insert initialLengthCoder directly - mh = MethodHandles.insertArguments(mh, 0, initialLengthCoder); - } - return mh; - } - - static int[] filterMixerArgPositions(int[] argPositions, int pos, int count) { - if (argPositions == null || argPositions.length != count + 2) { - argPositions = new int[count + 1]; - argPositions[0] = 0; // indexCoder - } - int limit = count + 1; - for (int i = 1; i < limit; i++) { - argPositions[i] = i + pos; - } - return argPositions; - } - - private static MethodHandle foldInLastMixers(MethodHandle mh, long initialLengthCoder, int pos, Class[] ptypes, int count) { - MethodHandle mix = switch (count) { - case 1 -> mixer(ptypes[pos]); - case 2 -> mixer(ptypes[pos], ptypes[pos + 1]); - case 3 -> mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2]); - case 4 -> mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - default -> throw new IllegalArgumentException("Unexpected count: " + count); - }; - mix = MethodHandles.insertArguments(mix,0, initialLengthCoder); - // apply selected arguments on the 1-4 arg mixer and fold in the result - return switch (count) { - case 1 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos); - case 2 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos); - case 3 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos, 3 + pos); - case 4 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos, 3 + pos, 4 + pos); - default -> throw new IllegalArgumentException(); - }; - } - - // Simple prependers, single argument. May be used directly or as a - // building block for complex prepender combinators. - private static MethodHandle prepender(String prefix, Class cl) { - if (prefix == null || prefix.isEmpty()) { - return noPrefixPrepender(cl); - } else { - return MethodHandles.insertArguments( - prepender(cl), 3, prefix); - } - } - - private static MethodHandle prepender(Class cl) { - int idx = classIndex(cl); - MethodHandle prepend = PREPENDERS[idx]; - if (prepend == null) { - PREPENDERS[idx] = prepend = JLA.stringConcatHelper("prepend", - methodType(long.class, long.class, byte[].class, - Wrapper.asPrimitiveType(cl), String.class)).rebind(); - } - return prepend; - } - - private static MethodHandle noPrefixPrepender(Class cl) { - int idx = classIndex(cl); - MethodHandle prepend = NO_PREFIX_PREPENDERS[idx]; - if (prepend == null) { - NO_PREFIX_PREPENDERS[idx] = prepend = MethodHandles.insertArguments(prepender(cl), 3, ""); - } - return prepend; - } - - private static final int INT_IDX = 0, - CHAR_IDX = 1, - LONG_IDX = 2, - BOOLEAN_IDX = 3, - STRING_IDX = 4, - TYPE_COUNT = 5; - private static int classIndex(Class cl) { - if (cl == String.class) return STRING_IDX; - if (cl == int.class) return INT_IDX; - if (cl == boolean.class) return BOOLEAN_IDX; - if (cl == char.class) return CHAR_IDX; - if (cl == long.class) return LONG_IDX; - throw new IllegalArgumentException("Unexpected class: " + cl); - } - - // Constant argument lists used by the prepender MH builders - private static final int[] PREPEND_FILTER_FIRST_ARGS = new int[] { 0, 1, 2 }; - private static final int[] PREPEND_FILTER_SECOND_ARGS = new int[] { 0, 1, 3 }; - private static final int[] PREPEND_FILTER_THIRD_ARGS = new int[] { 0, 1, 4 }; - private static final int[] PREPEND_FILTER_FIRST_PAIR_ARGS = new int[] { 0, 1, 2, 3 }; - private static final int[] PREPEND_FILTER_SECOND_PAIR_ARGS = new int[] { 0, 1, 4, 5 }; - - // Base MH for complex prepender combinators. - private static @Stable MethodHandle PREPEND_BASE; - private static MethodHandle prependBase() { - MethodHandle base = PREPEND_BASE; - if (base == null) { - base = PREPEND_BASE = MethodHandles.dropArguments( - MethodHandles.identity(long.class), 1, byte[].class); - } - return base; - } - - private static final @Stable MethodHandle[][] DOUBLE_PREPENDERS = new MethodHandle[TYPE_COUNT][TYPE_COUNT]; - - private static MethodHandle prepender(String prefix, Class cl, String prefix2, Class cl2) { - int idx1 = classIndex(cl); - int idx2 = classIndex(cl2); - MethodHandle prepend = DOUBLE_PREPENDERS[idx1][idx2]; - if (prepend == null) { - prepend = DOUBLE_PREPENDERS[idx1][idx2] = - MethodHandles.dropArguments(prependBase(), 2, cl, cl2); - } - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, prepender(prefix, cl), - PREPEND_FILTER_FIRST_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, prepender(prefix2, cl2), - PREPEND_FILTER_SECOND_ARGS); - } - - private static MethodHandle prepender(int pos, String[] constants, Class[] ptypes, int count) { - // build the simple cases directly - if (count == 1) { - return prepender(constants[pos], ptypes[pos]); - } - if (count == 2) { - return prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]); - } - // build a tree from an unbound prepender, allowing us to bind the constants in a batch as a final step - MethodHandle prepend = prependBase(); - if (count == 3) { - prepend = MethodHandles.dropArguments(prepend, 2, - ptypes[pos], ptypes[pos + 1], ptypes[pos + 2]); - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]), - PREPEND_FILTER_FIRST_PAIR_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos + 2], ptypes[pos + 2]), - PREPEND_FILTER_THIRD_ARGS); - } else if (count == 4) { - prepend = MethodHandles.dropArguments(prepend, 2, - ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]), - PREPEND_FILTER_FIRST_PAIR_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos + 2], ptypes[pos + 2], constants[pos + 3], ptypes[pos + 3]), - PREPEND_FILTER_SECOND_PAIR_ARGS); - } else { - throw new IllegalArgumentException("Unexpected count: " + count); - } - } - - // Constant argument lists used by the mixer MH builders - private static final int[] MIX_FILTER_SECOND_ARGS = new int[] { 0, 2 }; - private static final int[] MIX_FILTER_THIRD_ARGS = new int[] { 0, 3 }; - private static final int[] MIX_FILTER_SECOND_PAIR_ARGS = new int[] { 0, 3, 4 }; - private static MethodHandle mixer(Class cl) { - int index = classIndex(cl); - MethodHandle mix = MIXERS[index]; - if (mix == null) { - MIXERS[index] = mix = JLA.stringConcatHelper("mix", - methodType(long.class, long.class, Wrapper.asPrimitiveType(cl))).rebind(); - } - return mix; - } - - private static final @Stable MethodHandle[][] DOUBLE_MIXERS = new MethodHandle[TYPE_COUNT][TYPE_COUNT]; - private static MethodHandle mixer(Class cl, Class cl2) { - int idx1 = classIndex(cl); - int idx2 = classIndex(cl2); - MethodHandle mix = DOUBLE_MIXERS[idx1][idx2]; - if (mix == null) { - mix = mixer(cl); - mix = MethodHandles.dropArguments(mix, 2, cl2); - DOUBLE_MIXERS[idx1][idx2] = mix = MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl2), MIX_FILTER_SECOND_ARGS); - } - return mix; - } - - private static MethodHandle mixer(Class cl, Class cl2, Class cl3) { - MethodHandle mix = mixer(cl, cl2); - mix = MethodHandles.dropArguments(mix, 3, cl3); - return MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl3), MIX_FILTER_THIRD_ARGS); - } - - private static MethodHandle mixer(Class cl, Class cl2, Class cl3, Class cl4) { - MethodHandle mix = mixer(cl, cl2); - mix = MethodHandles.dropArguments(mix, 3, cl3, cl4); - return MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl3, cl4), MIX_FILTER_SECOND_PAIR_ARGS); - } - private @Stable static MethodHandle SIMPLE_CONCAT; private static MethodHandle simpleConcat() { MethodHandle mh = SIMPLE_CONCAT; @@ -908,42 +521,11 @@ public final class StringConcatFactory { return mh; } - private @Stable static MethodHandle NEW_STRING; - private static MethodHandle newString() { - MethodHandle mh = NEW_STRING; - if (mh == null) { - MethodHandle newString = JLA.stringConcatHelper("newString", - methodType(String.class, byte[].class, long.class)); - NEW_STRING = mh = newString.rebind(); - } - return mh; - } - - private @Stable static MethodHandle NEW_ARRAY_SUFFIX; - private static MethodHandle newArrayWithSuffix(String suffix) { - MethodHandle mh = NEW_ARRAY_SUFFIX; - if (mh == null) { - MethodHandle newArrayWithSuffix = JLA.stringConcatHelper("newArrayWithSuffix", - methodType(byte[].class, String.class, long.class)); - NEW_ARRAY_SUFFIX = mh = newArrayWithSuffix.rebind(); - } - return MethodHandles.insertArguments(mh, 0, suffix); - } - /** * Public gateways to public "stringify" methods. These methods have the * form String apply(T obj), and normally delegate to {@code String.valueOf}, * depending on argument's type. */ - private @Stable static MethodHandle OBJECT_STRINGIFIER; - private static MethodHandle objectStringifier() { - MethodHandle mh = OBJECT_STRINGIFIER; - if (mh == null) { - OBJECT_STRINGIFIER = mh = JLA.stringConcatHelper("stringOf", - methodType(String.class, Object.class)); - } - return mh; - } private @Stable static MethodHandle FLOAT_STRINGIFIER; private static MethodHandle floatStringifier() { MethodHandle mh = FLOAT_STRINGIFIER; @@ -1027,38 +609,6 @@ public final class StringConcatFactory { } } - private static final @Stable MethodHandle[] NO_PREFIX_PREPENDERS = new MethodHandle[TYPE_COUNT]; - private static final @Stable MethodHandle[] PREPENDERS = new MethodHandle[TYPE_COUNT]; - private static final @Stable MethodHandle[] MIXERS = new MethodHandle[TYPE_COUNT]; - private static final long INITIAL_CODER = JLA.stringConcatInitialCoder(); - - /** - * Promote integral types to int. - */ - private static Class promoteToIntType(Class t) { - // use int for subword integral types; still need special mixers - // and prependers for char, boolean - return t == byte.class || t == short.class ? int.class : t; - } - - /** - * Returns a stringifier for references and floats/doubles only. - * Always returns null for other primitives. - * - * @param t class to stringify - * @return stringifier; null, if not available - */ - private static MethodHandle stringifierFor(Class t) { - if (t == Object.class) { - return objectStringifier(); - } else if (t == float.class) { - return floatStringifier(); - } else if (t == double.class) { - return doubleStringifier(); - } - return null; - } - private static MethodHandle stringValueOf(Class ptype) { try { return MethodHandles.publicLookup() diff --git a/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java b/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java index c55ccd735df..d5f4470d9c9 100644 --- a/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java +++ b/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java @@ -54,6 +54,8 @@ import static java.util.Objects.*; import jdk.internal.module.Checks; import jdk.internal.module.ModuleInfo; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** @@ -91,6 +93,7 @@ import jdk.internal.module.ModuleInfo; * @since 9 */ +@AOTSafeClassInitializer public final class ModuleDescriptor implements Comparable { @@ -2665,6 +2668,11 @@ public final class ModuleDescriptor } static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { /** * Setup the shared secret to allow code in other packages access * private package methods in java.lang.module. diff --git a/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java b/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java index f4d82595842..99716baf439 100644 --- a/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java +++ b/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java @@ -165,22 +165,22 @@ public final class SwitchBootstraps { * @param lookup Represents a lookup context with the accessibility * privileges of the caller. When used with {@code invokedynamic}, * this is stacked automatically by the VM. - * @param invocationName unused + * @param invocationName unused, {@code null} is permitted * @param invocationType The invocation type of the {@code CallSite} with two parameters, * a reference type, an {@code int}, and {@code int} as a return type. * @param labels case labels - {@code String} and {@code Integer} constants * and {@code Class} and {@code EnumDesc} instances, in any combination * @return a {@code CallSite} returning the first matching element as described above * - * @throws NullPointerException if any argument is {@code null} + * @throws NullPointerException if any argument is {@code null}, unless noted otherwise * @throws IllegalArgumentException if any element in the labels array is null * @throws IllegalArgumentException if the invocation type is not a method type of first parameter of a reference type, - * second parameter of type {@code int} and with {@code int} as its return type, + * second parameter of type {@code int} and with {@code int} as its return type * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code String}, * {@code Integer}, {@code Long}, {@code Float}, {@code Double}, {@code Boolean}, - * {@code Class} or {@code EnumDesc}. + * {@code Class} or {@code EnumDesc} * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code Boolean} - * when {@code target} is a {@code Boolean.class}. + * when {@code target} is a {@code Boolean.class} * @jvms 4.4.6 The CONSTANT_NameAndType_info Structure * @jvms 4.4.10 The CONSTANT_Dynamic_info and CONSTANT_InvokeDynamic_info Structures */ @@ -255,29 +255,36 @@ public final class SwitchBootstraps { * enum constant's {@link Enum#name()}. * *

    - * If no element in the {@code labels} array matches the target, then - * the method of the call site return the length of the {@code labels} array. + * If for a given {@code target} there is no element in the {@code labels} + * fulfilling one of the above conditions, then the method of the call + * site returns the length of the {@code labels} array. *

    * The value of the {@code restart} index must be between {@code 0} (inclusive) and * the length of the {@code labels} array (inclusive), - * both or an {@link IndexOutOfBoundsException} is thrown. + * or an {@link IndexOutOfBoundsException} is thrown. + * + * @apiNote It is permissible for the {@code labels} array to contain {@code String} + * values that do not represent any enum constants at runtime. * * @param lookup Represents a lookup context with the accessibility * privileges of the caller. When used with {@code invokedynamic}, * this is stacked automatically by the VM. - * @param invocationName unused + * @param invocationName unused, {@code null} is permitted * @param invocationType The invocation type of the {@code CallSite} with two parameters, * an enum type, an {@code int}, and {@code int} as a return type. * @param labels case labels - {@code String} constants and {@code Class} instances, * in any combination * @return a {@code CallSite} returning the first matching element as described above * - * @throws NullPointerException if any argument is {@code null} - * @throws IllegalArgumentException if any element in the labels array is null, if the - * invocation type is not a method type whose first parameter type is an enum type, - * second parameter of type {@code int} and whose return type is {@code int}, - * or if {@code labels} contains an element that is not of type {@code String} or - * {@code Class} of the target enum type. + * @throws NullPointerException if any argument is {@code null}, unless noted otherwise + * @throws IllegalArgumentException if any element in the labels array is null + * @throws IllegalArgumentException if any element in the labels array is an empty {@code String} + * @throws IllegalArgumentException if the invocation type is not a method type + * whose first parameter type is an enum type, + * second parameter of type {@code int} and + * whose return type is {@code int} + * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code String} or + * {@code Class} equal to the target enum type * @jvms 4.4.6 The CONSTANT_NameAndType_info Structure * @jvms 4.4.10 The CONSTANT_Dynamic_info and CONSTANT_InvokeDynamic_info Structures */ diff --git a/src/java.base/share/classes/java/math/BigInteger.java b/src/java.base/share/classes/java/math/BigInteger.java index 21f8598266f..6253adffb2b 100644 --- a/src/java.base/share/classes/java/math/BigInteger.java +++ b/src/java.base/share/classes/java/math/BigInteger.java @@ -4180,6 +4180,10 @@ public class BigInteger extends Number implements Comparable { if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10; + if (fitsIntoLong()) { + return Long.toString(longValue(), radix); + } + BigInteger abs = this.abs(); // Ensure buffer capacity sufficient to contain string representation @@ -5121,12 +5125,16 @@ public class BigInteger extends Number implements Comparable { * @since 1.8 */ public long longValueExact() { - if (mag.length <= 2 && bitLength() < Long.SIZE) + if (fitsIntoLong()) return longValue(); throw new ArithmeticException("BigInteger out of long range"); } + private boolean fitsIntoLong() { + return mag.length <= 2 && bitLength() < Long.SIZE; + } + /** * Converts this {@code BigInteger} to an {@code int}, checking * for lost information. If the value of this {@code BigInteger} diff --git a/src/java.base/share/classes/java/net/URI.java b/src/java.base/share/classes/java/net/URI.java index daf63d19032..d130dc3b460 100644 --- a/src/java.base/share/classes/java/net/URI.java +++ b/src/java.base/share/classes/java/net/URI.java @@ -43,6 +43,8 @@ import java.text.Normalizer; import jdk.internal.access.JavaNetUriAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.Exceptions; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import sun.nio.cs.UTF_8; import static jdk.internal.util.Exceptions.filterNonSocketInfo; @@ -516,6 +518,7 @@ import static jdk.internal.util.Exceptions.formatMsg; * @see URISyntaxException */ +@AOTSafeClassInitializer public final class URI implements Comparable, Serializable { @@ -3726,7 +3729,13 @@ public final class URI } } + static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { SharedSecrets.setJavaNetUriAccess( new JavaNetUriAccess() { public URI create(String scheme, String path) { diff --git a/src/java.base/share/classes/java/net/URL.java b/src/java.base/share/classes/java/net/URL.java index 9266b6c94f1..c82236b5b85 100644 --- a/src/java.base/share/classes/java/net/URL.java +++ b/src/java.base/share/classes/java/net/URL.java @@ -41,8 +41,9 @@ import java.util.ServiceLoader; import jdk.internal.access.JavaNetURLAccess; import jdk.internal.access.SharedSecrets; -import jdk.internal.misc.ThreadTracker; import jdk.internal.misc.VM; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import sun.net.util.IPAddressUtil; import static jdk.internal.util.Exceptions.filterNonSocketInfo; import static jdk.internal.util.Exceptions.formatMsg; @@ -214,6 +215,7 @@ import static jdk.internal.util.Exceptions.formatMsg; * @author James Gosling * @since 1.0 */ +@AOTSafeClassInitializer public final class URL implements java.io.Serializable { static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol"; @@ -1391,24 +1393,13 @@ public final class URL implements java.io.Serializable { return handler; } - private static class ThreadTrackHolder { - static final ThreadTracker TRACKER = new ThreadTracker(); - } - - private static Object tryBeginLookup() { - return ThreadTrackHolder.TRACKER.tryBegin(); - } - - private static void endLookup(Object key) { - ThreadTrackHolder.TRACKER.end(key); - } + private static final ScopedValue IN_LOOKUP = ScopedValue.newInstance(); private static URLStreamHandler lookupViaProviders(final String protocol) { - Object key = tryBeginLookup(); - if (key == null) { + if (IN_LOOKUP.isBound()) { throw new Error("Circular loading of URL stream handler providers detected"); } - try { + return ScopedValue.where(IN_LOOKUP, true).call(() -> { final ClassLoader cl = ClassLoader.getSystemClassLoader(); final ServiceLoader sl = ServiceLoader.load(URLStreamHandlerProvider.class, cl); @@ -1420,9 +1411,7 @@ public final class URL implements java.io.Serializable { return h; } return null; - } finally { - endLookup(key); - } + }); } /** @@ -1758,6 +1747,11 @@ public final class URL implements java.io.Serializable { } static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { SharedSecrets.setJavaNetURLAccess( new JavaNetURLAccess() { @Override diff --git a/src/java.base/share/classes/java/nio/BufferOverflowException.java b/src/java.base/share/classes/java/nio/BufferOverflowException.java new file mode 100644 index 00000000000..22195f8268b --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferOverflowException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a relative put operation reaches + * the target buffer's limit. + * + * @since 1.4 + */ + +public class BufferOverflowException + extends RuntimeException +{ + + @java.io.Serial + private static final long serialVersionUID = -5484897634319144535L; + + /** + * Constructs an instance of this class. + */ + public BufferOverflowException() { } +} diff --git a/src/java.base/share/classes/java/nio/BufferUnderflowException.java b/src/java.base/share/classes/java/nio/BufferUnderflowException.java new file mode 100644 index 00000000000..1561f1d4b62 --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferUnderflowException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a relative get operation reaches + * the source buffer's limit. + * + * @since 1.4 + */ + +public class BufferUnderflowException + extends RuntimeException +{ + + @java.io.Serial + private static final long serialVersionUID = -1713313658691622206L; + + /** + * Constructs an instance of this class. + */ + public BufferUnderflowException() { } +} diff --git a/src/java.base/share/classes/java/nio/ByteOrder.java b/src/java.base/share/classes/java/nio/ByteOrder.java index 96f2317b956..ab6876448be 100644 --- a/src/java.base/share/classes/java/nio/ByteOrder.java +++ b/src/java.base/share/classes/java/nio/ByteOrder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,28 +35,19 @@ import jdk.internal.misc.Unsafe; * @since 1.4 */ -public final class ByteOrder { - - private final String name; - - private ByteOrder(String name) { - this.name = name; - } - - /** - * Constant denoting big-endian byte order. In this order, the bytes of a - * multibyte value are ordered from most significant to least significant. - */ - public static final ByteOrder BIG_ENDIAN - = new ByteOrder("BIG_ENDIAN"); - +public enum ByteOrder { /** * Constant denoting little-endian byte order. In this order, the bytes of * a multibyte value are ordered from least significant to most * significant. */ - public static final ByteOrder LITTLE_ENDIAN - = new ByteOrder("LITTLE_ENDIAN"); + LITTLE_ENDIAN, + /** + * Constant denoting big-endian byte order. In this order, the bytes of a + * multibyte value are ordered from most significant to least significant. + */ + BIG_ENDIAN; + // Retrieve the native byte order. It's used early during bootstrap, and // must be initialized after BIG_ENDIAN and LITTLE_ENDIAN. @@ -78,18 +69,4 @@ public final class ByteOrder { public static ByteOrder nativeOrder() { return NATIVE_ORDER; } - - /** - * Constructs a string describing this object. - * - *

    This method returns the string - * {@code "BIG_ENDIAN"} for {@link #BIG_ENDIAN} and - * {@code "LITTLE_ENDIAN"} for {@link #LITTLE_ENDIAN}. - * - * @return The specified string - */ - public String toString() { - return name; - } - } diff --git a/src/java.base/share/classes/java/nio/InvalidMarkException.java b/src/java.base/share/classes/java/nio/InvalidMarkException.java new file mode 100644 index 00000000000..8ee16693c0d --- /dev/null +++ b/src/java.base/share/classes/java/nio/InvalidMarkException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when an attempt is made to reset a buffer + * when its mark is not defined. + * + * @since 1.4 + */ + +public class InvalidMarkException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 1698329710438510774L; + + /** + * Constructs an instance of this class. + */ + public InvalidMarkException() { } +} diff --git a/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java b/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java new file mode 100644 index 00000000000..e86123307f3 --- /dev/null +++ b/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a content-mutation method such as + * put or compact is invoked upon a read-only buffer. + * + * @since 1.4 + */ + +public class ReadOnlyBufferException + extends UnsupportedOperationException +{ + + @java.io.Serial + private static final long serialVersionUID = -1210063976496234090L; + + /** + * Constructs an instance of this class. + */ + public ReadOnlyBufferException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java b/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java new file mode 100644 index 00000000000..d02b3f289b9 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to initiate an accept + * operation on a channel and a previous accept operation has not completed. + * + * @since 1.7 + */ + +public class AcceptPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2721339977965416421L; + + /** + * Constructs an instance of this class. + */ + public AcceptPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java b/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java new file mode 100644 index 00000000000..4602e8d1f46 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to bind the socket a + * network oriented channel that is already bound. + * + * @since 1.7 + */ + +public class AlreadyBoundException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 6796072983322737592L; + + /** + * Constructs an instance of this class. + */ + public AlreadyBoundException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java b/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java new file mode 100644 index 00000000000..48fe018a0b7 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to connect a {@link + * SocketChannel} that is already connected. + * + * @since 1.4 + */ + +public class AlreadyConnectedException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -7331895245053773357L; + + /** + * Constructs an instance of this class. + */ + public AlreadyConnectedException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java b/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java new file mode 100644 index 00000000000..bc3ee02f50b --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread closes the + * channel or the part of the channel upon which it is blocked in an I/O + * operation. + * + * @since 1.4 + */ + +public class AsynchronousCloseException + extends ClosedChannelException +{ + + @java.io.Serial + private static final long serialVersionUID = 6891178312432313966L; + + /** + * Constructs an instance of this class. + */ + public AsynchronousCloseException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java b/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java new file mode 100644 index 00000000000..946ea7205b1 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to use + * a selection key that is no longer valid. + * + * @since 1.4 + */ + +public class CancelledKeyException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -8438032138028814268L; + + /** + * Constructs an instance of this class. + */ + public CancelledKeyException() { } +} diff --git a/src/hotspot/share/gc/shared/strongRootsScope.cpp b/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java similarity index 52% rename from src/hotspot/share/gc/shared/strongRootsScope.cpp rename to src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java index 1316df68e5f..c612458c9e4 100644 --- a/src/hotspot/share/gc/shared/strongRootsScope.cpp +++ b/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,33 +21,28 @@ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. - * */ -#include "classfile/stringTable.hpp" -#include "code/nmethod.hpp" -#include "gc/shared/strongRootsScope.hpp" -#include "runtime/threads.hpp" +package java.nio.channels; -MarkScope::MarkScope() { - nmethod::oops_do_marking_prologue(); -} +/** + * Checked exception received by a thread when another thread interrupts it + * while it is blocked in an I/O operation upon a channel. Before this + * exception is thrown the channel will have been closed and the interrupt + * status of the previously-blocked thread will have been set. + * + * @since 1.4 + */ -MarkScope::~MarkScope() { - nmethod::oops_do_marking_epilogue(); -} +public class ClosedByInterruptException + extends AsynchronousCloseException +{ -StrongRootsScope::StrongRootsScope(uint n_threads) : _n_threads(n_threads) { - // No need for thread claim for statically-known sequential case (_n_threads == 0) - // For positive values, clients of this class often unify sequential/parallel - // cases, so they expect the thread claim token to be updated. - if (_n_threads != 0) { - Threads::change_thread_claim_token(); - } -} + @java.io.Serial + private static final long serialVersionUID = -4488191543534286750L; -StrongRootsScope::~StrongRootsScope() { - if (_n_threads != 0) { - Threads::assert_all_threads_claimed(); - } + /** + * Constructs an instance of this class. + */ + public ClosedByInterruptException() { } } diff --git a/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java b/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java new file mode 100644 index 00000000000..71452b6d1c2 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception thrown when an attempt is made to invoke or complete an + * I/O operation upon channel that is closed, or at least closed to that + * operation. That this exception is thrown does not necessarily imply that + * the channel is completely closed. A socket channel whose write half has + * been shut down, for example, may still be open for reading. + * + * @since 1.4 + */ + +public class ClosedChannelException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 882777185433553857L; + + /** + * Constructs an instance of this class. + */ + public ClosedChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java b/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java new file mode 100644 index 00000000000..dcefb98d69e --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a closed selector. + * + * @since 1.4 + */ + +public class ClosedSelectorException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 6466297122317847835L; + + /** + * Constructs an instance of this class. + */ + public ClosedSelectorException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java b/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java new file mode 100644 index 00000000000..2a5b80e3b25 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to connect a {@link + * SocketChannel} for which a non-blocking connection operation is already in + * progress. + * + * @since 1.4 + */ + +public class ConnectionPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2008393366501760879L; + + /** + * Constructs an instance of this class. + */ + public ConnectionPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java b/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java new file mode 100644 index 00000000000..7ecae1b4a46 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread interrupts it + * while it is waiting to acquire a file lock. Before this exception is thrown + * the interrupt status of the previously-blocked thread will have been set. + * + * @since 1.4 + */ + +public class FileLockInterruptionException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 7104080643653532383L; + + /** + * Constructs an instance of this class. + */ + public FileLockInterruptionException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java b/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java new file mode 100644 index 00000000000..28d1627ef34 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when a blocking-mode-specific operation + * is invoked upon a channel in the incorrect blocking mode. + * + * @since 1.4 + */ + +public class IllegalBlockingModeException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3335774961855590474L; + + /** + * Constructs an instance of this class. + */ + public IllegalBlockingModeException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java b/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java new file mode 100644 index 00000000000..3e5da2b87c4 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to open a channel + * in a group that was not created by the same provider. + * + * @since 1.7 + */ + +public class IllegalChannelGroupException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -2495041211157744253L; + + /** + * Constructs an instance of this class. + */ + public IllegalChannelGroupException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java b/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java new file mode 100644 index 00000000000..d1e26c3352f --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to register a channel + * with a selector that was not created by the provider that created the + * channel. + * + * @since 1.4 + */ + +public class IllegalSelectorException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -8406323347253320987L; + + /** + * Constructs an instance of this class. + */ + public IllegalSelectorException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java b/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java new file mode 100644 index 00000000000..3dc6931e7fb --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when a timeout elapses before an + * asynchronous operation completes. + * + * @since 1.7 + */ + +public class InterruptedByTimeoutException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = -4268008601014042947L; + + /** + * Constructs an instance of this class. + */ + public InterruptedByTimeoutException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java b/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java new file mode 100644 index 00000000000..954486e4d35 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when the {@link SocketChannel#finishConnect + * finishConnect} method of a {@link SocketChannel} is invoked without first + * successfully invoking its {@link SocketChannel#connect connect} method. + * + * @since 1.4 + */ + +public class NoConnectionPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -8296561183633134743L; + + /** + * Constructs an instance of this class. + */ + public NoConnectionPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java b/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java new file mode 100644 index 00000000000..4714ace0b30 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to read + * from a channel that was not originally opened for reading. + * + * @since 1.4 + */ + +public class NonReadableChannelException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3200915679294993514L; + + /** + * Constructs an instance of this class. + */ + public NonReadableChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java b/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java new file mode 100644 index 00000000000..70877bb46b9 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to write + * to a channel that was not originally opened for writing. + * + * @since 1.4 + */ + +public class NonWritableChannelException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -7071230488279011621L; + + /** + * Constructs an instance of this class. + */ + public NonWritableChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java b/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java new file mode 100644 index 00000000000..12f222b7692 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a server socket channel that is not yet bound. + * + * @since 1.4 + */ + +public class NotYetBoundException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 4640999303950202242L; + + /** + * Constructs an instance of this class. + */ + public NotYetBoundException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java b/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java new file mode 100644 index 00000000000..8ea6150de7d --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a socket channel that is not yet connected. + * + * @since 1.4 + */ + +public class NotYetConnectedException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 4697316551909513464L; + + /** + * Constructs an instance of this class. + */ + public NotYetConnectedException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java b/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java new file mode 100644 index 00000000000..a24019ffcd1 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to acquire a lock on a + * region of a file that overlaps a region already locked by the same Java + * virtual machine, or when another thread is already waiting to lock an + * overlapping region of the same file. + * + * @since 1.4 + */ + +public class OverlappingFileLockException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2047812138163068433L; + + /** + * Constructs an instance of this class. + */ + public OverlappingFileLockException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ReadPendingException.java b/src/java.base/share/classes/java/nio/channels/ReadPendingException.java new file mode 100644 index 00000000000..113c3fb27a6 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ReadPendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to read from an + * asynchronous socket channel and a previous read has not completed. + * + * @since 1.7 + */ + +public class ReadPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 1986315242191227217L; + + /** + * Constructs an instance of this class. + */ + public ReadPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java b/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java new file mode 100644 index 00000000000..d25f6f568ae --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to construct a channel in + * a group that is shutdown or the completion handler for an I/O operation + * cannot be invoked because the channel group has terminated. + * + * @since 1.7 + */ + +public class ShutdownChannelGroupException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3903801676350154157L; + + /** + * Constructs an instance of this class. + */ + public ShutdownChannelGroupException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java b/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java new file mode 100644 index 00000000000..86ae3e6d7a6 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke a network + * operation upon an unresolved socket address. + * + * @since 1.4 + */ + +public class UnresolvedAddressException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 6136959093620794148L; + + /** + * Constructs an instance of this class. + */ + public UnresolvedAddressException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java b/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java new file mode 100644 index 00000000000..0b63cd8d418 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to bind or connect + * to a socket address of a type that is not supported. + * + * @since 1.4 + */ + +public class UnsupportedAddressTypeException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -2964323842829700493L; + + /** + * Constructs an instance of this class. + */ + public UnsupportedAddressTypeException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/WritePendingException.java b/src/java.base/share/classes/java/nio/channels/WritePendingException.java new file mode 100644 index 00000000000..f4dca987247 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/WritePendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to write to an + * asynchronous socket channel and a previous write has not completed. + * + * @since 1.7 + */ + +public class WritePendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 7031871839266032276L; + + /** + * Constructs an instance of this class. + */ + public WritePendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/exceptions b/src/java.base/share/classes/java/nio/channels/exceptions deleted file mode 100644 index 3f75fb3fcd2..00000000000 --- a/src/java.base/share/classes/java/nio/channels/exceptions +++ /dev/null @@ -1,194 +0,0 @@ -# -# Copyright (c) 2000, 2009, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generated exception classes for java.nio.channels - -SINCE=1.4 -PACKAGE=java.nio.channels -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2007," - - -SUPER=java.io.IOException - -gen ClosedChannelException " - * Checked exception thrown when an attempt is made to invoke or complete an - * I/O operation upon channel that is closed, or at least closed to that - * operation. That this exception is thrown does not necessarily imply that - * the channel is completely closed. A socket channel whose write half has - * been shut down, for example, may still be open for reading." \ - 882777185433553857L - -gen FileLockInterruptionException " - * Checked exception received by a thread when another thread interrupts it - * while it is waiting to acquire a file lock. Before this exception is thrown - * the interrupt status of the previously-blocked thread will have been set." \ - 7104080643653532383L - - -SUPER=ClosedChannelException - -gen AsynchronousCloseException " - * Checked exception received by a thread when another thread closes the - * channel or the part of the channel upon which it is blocked in an I/O - * operation." \ - 6891178312432313966L - - -SUPER=AsynchronousCloseException - -gen ClosedByInterruptException " - * Checked exception received by a thread when another thread interrupts it - * while it is blocked in an I/O operation upon a channel. Before this - * exception is thrown the channel will have been closed and the interrupt - * status of the previously-blocked thread will have been set." \ - -4488191543534286750L - - -SUPER=IllegalArgumentException - -gen IllegalSelectorException " - * Unchecked exception thrown when an attempt is made to register a channel - * with a selector that was not created by the provider that created the - * channel." \ - -8406323347253320987L - -gen UnresolvedAddressException " - * Unchecked exception thrown when an attempt is made to invoke a network - * operation upon an unresolved socket address." \ - 6136959093620794148L - -gen UnsupportedAddressTypeException " - * Unchecked exception thrown when an attempt is made to bind or connect - * to a socket address of a type that is not supported." \ - -2964323842829700493L - - -SUPER=IllegalStateException - -gen AlreadyConnectedException " - * Unchecked exception thrown when an attempt is made to connect a {@link - * SocketChannel} that is already connected." \ - -7331895245053773357L - -gen ConnectionPendingException " - * Unchecked exception thrown when an attempt is made to connect a {@link - * SocketChannel} for which a non-blocking connection operation is already in - * progress." \ - 2008393366501760879L - -gen ClosedSelectorException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a closed selector." \ - 6466297122317847835L - -gen CancelledKeyException " - * Unchecked exception thrown when an attempt is made to use - * a selection key that is no longer valid." \ - -8438032138028814268L - -gen IllegalBlockingModeException " - * Unchecked exception thrown when a blocking-mode-specific operation - * is invoked upon a channel in the incorrect blocking mode." \ - -3335774961855590474L - -gen NoConnectionPendingException " - * Unchecked exception thrown when the {@link SocketChannel#finishConnect - * finishConnect} method of a {@link SocketChannel} is invoked without first - * successfully invoking its {@link SocketChannel#connect connect} method." \ - -8296561183633134743L - -gen NonReadableChannelException " - * Unchecked exception thrown when an attempt is made to read - * from a channel that was not originally opened for reading." \ - -3200915679294993514L - -gen NonWritableChannelException " - * Unchecked exception thrown when an attempt is made to write - * to a channel that was not originally opened for writing." \ - -7071230488279011621L - -gen NotYetBoundException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a server socket channel that is not yet bound." \ - 4640999303950202242L - -gen NotYetConnectedException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a socket channel that is not yet connected." \ - 4697316551909513464L - -gen OverlappingFileLockException " - * Unchecked exception thrown when an attempt is made to acquire a lock on a - * region of a file that overlaps a region already locked by the same Java - * virtual machine, or when another thread is already waiting to lock an - * overlapping region of the same file." \ - 2047812138163068433L - - -SINCE=1.7 - -SUPER=java.io.IOException - -gen InterruptedByTimeoutException " - * Checked exception received by a thread when a timeout elapses before an - * asynchronous operation completes." \ - -4268008601014042947L - -SUPER=IllegalArgumentException - -gen IllegalChannelGroupException " - * Unchecked exception thrown when an attempt is made to open a channel - * in a group that was not created by the same provider. " \ - -2495041211157744253L - - -SUPER=IllegalStateException - -gen AlreadyBoundException " - * Unchecked exception thrown when an attempt is made to bind the socket a - * network oriented channel that is already bound." \ - 6796072983322737592L - -gen AcceptPendingException " - * Unchecked exception thrown when an attempt is made to initiate an accept - * operation on a channel and a previous accept operation has not completed." \ - 2721339977965416421L - -gen ReadPendingException " - * Unchecked exception thrown when an attempt is made to read from an - * asynchronous socket channel and a previous read has not completed." \ - 1986315242191227217L - -gen WritePendingException " - * Unchecked exception thrown when an attempt is made to write to an - * asynchronous socket channel and a previous write has not completed." \ - 7031871839266032276L - -gen ShutdownChannelGroupException " - * Unchecked exception thrown when an attempt is made to construct a channel in - * a group that is shutdown or the completion handler for an I/O operation - * cannot be invoked because the channel group has terminated." \ - -3903801676350154157L diff --git a/src/hotspot/share/gc/shared/bufferNodeList.cpp b/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java similarity index 59% rename from src/hotspot/share/gc/shared/bufferNodeList.cpp rename to src/java.base/share/classes/java/nio/charset/CharacterCodingException.java index 768f40e0985..00a8efae4dd 100644 --- a/src/hotspot/share/gc/shared/bufferNodeList.cpp +++ b/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,20 +21,26 @@ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. - * */ -#include "gc/shared/bufferNodeList.hpp" -#include "utilities/debug.hpp" +package java.nio.charset; -BufferNodeList::BufferNodeList() : - _head(nullptr), _tail(nullptr), _entry_count(0) {} +/** + * Checked exception thrown when a character encoding + * or decoding error occurs. + * + * @since 1.4 + */ -BufferNodeList::BufferNodeList(BufferNode* head, - BufferNode* tail, - size_t entry_count) : - _head(head), _tail(tail), _entry_count(entry_count) +public class CharacterCodingException + extends java.io.IOException { - assert((_head == nullptr) == (_tail == nullptr), "invariant"); - assert((_head == nullptr) == (_entry_count == 0), "invariant"); + + @java.io.Serial + private static final long serialVersionUID = 8421532232154627783L; + + /** + * Constructs an instance of this class. + */ + public CharacterCodingException() { } } diff --git a/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java b/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java new file mode 100644 index 00000000000..992168883bb --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.charset; + +/** + * Unchecked exception thrown when a string that is not a + * legal charset name is used as such. + * + * @since 1.4 + */ + +public class IllegalCharsetNameException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 1457525358470002989L; + + /** + * The illegal charset name. + * + * @serial + */ + private String charsetName; + + /** + * Constructs an instance of this class. + * + * @param charsetName + * The illegal charset name + */ + public IllegalCharsetNameException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /** + * Retrieves the illegal charset name. + * + * @return The illegal charset name + */ + public String getCharsetName() { + return charsetName; + } +} diff --git a/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java b/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java new file mode 100644 index 00000000000..da50e0cfd0c --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.charset; + +/** + * Unchecked exception thrown when no support is available + * for a requested charset. + * + * @since 1.4 + */ + +public class UnsupportedCharsetException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 1490765524727386367L; + + /** + * The name of the unsupported charset. + * + * @serial + */ + private String charsetName; + + /** + * Constructs an instance of this class. + * + * @param charsetName + * The name of the unsupported charset + */ + public UnsupportedCharsetException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /** + * Retrieves the name of the unsupported charset. + * + * @return The name of the unsupported charset + */ + public String getCharsetName() { + return charsetName; + } +} diff --git a/src/java.base/share/classes/java/nio/charset/exceptions b/src/java.base/share/classes/java/nio/charset/exceptions deleted file mode 100644 index c4773090ae0..00000000000 --- a/src/java.base/share/classes/java/nio/charset/exceptions +++ /dev/null @@ -1,53 +0,0 @@ -# -# Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generated exception classes for java.nio.charset - -SINCE=1.4 -PACKAGE=java.nio.charset -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2021," - -SUPER=java.io.IOException - -gen CharacterCodingException " - * Checked exception thrown when a character encoding - * or decoding error occurs." \ - 8421532232154627783L - - -SUPER=IllegalArgumentException - -gen IllegalCharsetNameException " - * Unchecked exception thrown when a string that is not a - * legal charset name is used as such." \ - 1457525358470002989L \ - String charsetName CharsetName "illegal charset name" - -gen UnsupportedCharsetException " - * Unchecked exception thrown when no support is available - * for a requested charset." \ - 1490765524727386367L \ - String charsetName CharsetName "name of the unsupported charset" diff --git a/src/java.base/share/classes/java/nio/exceptions b/src/java.base/share/classes/java/nio/exceptions deleted file mode 100644 index 7465cdbed78..00000000000 --- a/src/java.base/share/classes/java/nio/exceptions +++ /dev/null @@ -1,60 +0,0 @@ -# -# Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generated exception classes for java.nio - -SINCE=1.4 -PACKAGE=java.nio -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2021," - - -SUPER=RuntimeException - -gen BufferOverflowException " - * Unchecked exception thrown when a relative put operation reaches - * the target buffer's limit." \ - -5484897634319144535L - -gen BufferUnderflowException " - * Unchecked exception thrown when a relative get operation reaches - * the source buffer's limit." \ - -1713313658691622206L - - -SUPER=IllegalStateException - -gen InvalidMarkException " - * Unchecked exception thrown when an attempt is made to reset a buffer - * when its mark is not defined." \ - 1698329710438510774L - - -SUPER=UnsupportedOperationException - -gen ReadOnlyBufferException " - * Unchecked exception thrown when a content-mutation method such as - * put or compact is invoked upon a read-only buffer." \ - -1210063976496234090L diff --git a/src/java.base/share/classes/java/security/KeyStore.java b/src/java.base/share/classes/java/security/KeyStore.java index 9e50a1588e7..8f3d4ba29fd 100644 --- a/src/java.base/share/classes/java/security/KeyStore.java +++ b/src/java.base/share/classes/java/security/KeyStore.java @@ -37,6 +37,7 @@ import javax.security.auth.DestroyFailedException; import javax.security.auth.callback.*; import sun.security.util.Debug; +import sun.security.util.CryptoAlgorithmConstraints; /** * This class represents a storage facility for cryptographic @@ -841,12 +842,21 @@ public class KeyStore { * the {@link Security#getProviders() Security.getProviders()} method. * * @implNote - * The JDK Reference Implementation additionally uses the - * {@code jdk.security.provider.preferred} + * The JDK Reference Implementation additionally uses + *

      + *
    • the {@code jdk.security.provider.preferred} * {@link Security#getProperty(String) Security} property to determine - * the preferred provider order for the specified algorithm. This + * the preferred provider order for the specified keystore type. This * may be different from the order of providers returned by * {@link Security#getProviders() Security.getProviders()}. + *
    • + *
    • the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified keystore type is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + *
    • + *
    * * @param type the type of keystore. * See the KeyStore section in the Note that the list of registered providers may be retrieved via * the {@link Security#getProviders() Security.getProviders()} method. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified keystore type is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param type the type of keystore. * See the KeyStore section in the @@ -917,8 +940,15 @@ public class KeyStore { throws KeyStoreException, NoSuchProviderException { Objects.requireNonNull(type, "null type name"); - if (provider == null || provider.isEmpty()) + + if (provider == null || provider.isEmpty()) { throw new IllegalArgumentException("missing provider"); + } + + if (!CryptoAlgorithmConstraints.permits("KEYSTORE", type)) { + throw new KeyStoreException(type + " is disabled"); + } + try { Object[] objs = Security.getImpl(type, "KeyStore", provider); return new KeyStore((KeyStoreSpi)objs[0], (Provider)objs[1], type); @@ -935,6 +965,14 @@ public class KeyStore { * object is returned. Note that the specified provider object * does not have to be registered in the provider list. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified keystore type is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param type the type of keystore. * See the KeyStore section in the @@ -963,8 +1001,15 @@ public class KeyStore { throws KeyStoreException { Objects.requireNonNull(type, "null type name"); - if (provider == null) + + if (provider == null) { throw new IllegalArgumentException("missing provider"); + } + + if (!CryptoAlgorithmConstraints.permits("KEYSTORE", type)) { + throw new KeyStoreException(type + " is disabled"); + } + try { Object[] objs = Security.getImpl(type, "KeyStore", provider); return new KeyStore((KeyStoreSpi)objs[0], (Provider)objs[1], type); @@ -1677,6 +1722,14 @@ public class KeyStore { *

    Note that the list of registered providers may be retrieved via * the {@link Security#getProviders() Security.getProviders()} method. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified keystore type is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. Disallowed type will be skipped. + * * @param file the keystore file * @param password the keystore password, which may be {@code null} * @@ -1730,6 +1783,14 @@ public class KeyStore { *

    Note that the list of registered providers may be retrieved via * the {@link Security#getProviders() Security.getProviders()} method. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified keystore type is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. Disallowed type will be skipped. + * * @param file the keystore file * @param param the {@code LoadStoreParameter} that specifies how to load * the keystore, which may be {@code null} @@ -1798,8 +1859,12 @@ public class KeyStore { kdebug.println(s.getAlgorithm() + " keystore detected: " + file); } - keystore = new KeyStore(impl, p, s.getAlgorithm()); - break; + String ksAlgo = s.getAlgorithm(); + if (CryptoAlgorithmConstraints.permits( + "KEYSTORE", ksAlgo)) { + keystore = new KeyStore(impl, p, ksAlgo); + break; + } } } catch (NoSuchAlgorithmException e) { // ignore diff --git a/src/java.base/share/classes/java/security/MessageDigest.java b/src/java.base/share/classes/java/security/MessageDigest.java index fa8d3dea8fd..6e8f64f7ebe 100644 --- a/src/java.base/share/classes/java/security/MessageDigest.java +++ b/src/java.base/share/classes/java/security/MessageDigest.java @@ -33,6 +33,7 @@ import java.nio.ByteBuffer; import sun.security.jca.GetInstance; import sun.security.util.Debug; import sun.security.util.MessageDigestSpi2; +import sun.security.util.CryptoAlgorithmConstraints; import javax.crypto.SecretKey; @@ -155,12 +156,22 @@ public abstract class MessageDigest extends MessageDigestSpi { * the {@link Security#getProviders() Security.getProviders()} method. * * @implNote - * The JDK Reference Implementation additionally uses the - * {@code jdk.security.provider.preferred} + * The JDK Reference Implementation additionally uses the following + * security properties: + *

      + *
    • the {@code jdk.security.provider.preferred} * {@link Security#getProperty(String) Security} property to determine * the preferred provider order for the specified algorithm. This * may be different from the order of providers returned by * {@link Security#getProviders() Security.getProviders()}. + *
    • + *
    • the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + *
    • + *
    * * @param algorithm the name of the algorithm requested. * See the MessageDigest section in the
    Note that the list of registered providers may be retrieved via * the {@link Security#getProviders() Security.getProviders()} method. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param algorithm the name of the algorithm requested. * See the MessageDigest section in the @@ -246,12 +269,18 @@ public abstract class MessageDigest extends MessageDigestSpi { throws NoSuchAlgorithmException, NoSuchProviderException { Objects.requireNonNull(algorithm, "null algorithm name"); - if (provider == null || provider.isEmpty()) - throw new IllegalArgumentException("missing provider"); - MessageDigest md; + if (provider == null || provider.isEmpty()) { + throw new IllegalArgumentException("missing provider"); + } + + if (!CryptoAlgorithmConstraints.permits("MessageDigest", algorithm)) { + throw new NoSuchAlgorithmException(algorithm + " is disabled"); + } + GetInstance.Instance instance = GetInstance.getInstance("MessageDigest", MessageDigestSpi.class, algorithm, provider); + MessageDigest md; if (instance.impl instanceof MessageDigest messageDigest) { md = messageDigest; md.provider = instance.provider; @@ -271,6 +300,14 @@ public abstract class MessageDigest extends MessageDigestSpi { * is returned. Note that the specified provider does not * have to be registered in the provider list. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param algorithm the name of the algorithm requested. * See the MessageDigest section in the @@ -301,8 +338,15 @@ public abstract class MessageDigest extends MessageDigestSpi { throws NoSuchAlgorithmException { Objects.requireNonNull(algorithm, "null algorithm name"); - if (provider == null) + + if (provider == null) { throw new IllegalArgumentException("missing provider"); + } + + if (!CryptoAlgorithmConstraints.permits("MessageDigest", algorithm)) { + throw new NoSuchAlgorithmException(algorithm + " is disabled"); + } + Object[] objs = Security.getImpl(algorithm, "MessageDigest", provider); if (objs[0] instanceof MessageDigest md) { md.provider = (Provider)objs[1]; diff --git a/src/java.base/share/classes/java/security/SecureClassLoader.java b/src/java.base/share/classes/java/security/SecureClassLoader.java index b398d7332d7..7b0420ec601 100644 --- a/src/java.base/share/classes/java/security/SecureClassLoader.java +++ b/src/java.base/share/classes/java/security/SecureClassLoader.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import jdk.internal.misc.CDS; /** * This class extends {@code ClassLoader} with additional support for defining @@ -243,6 +244,20 @@ public class SecureClassLoader extends ClassLoader { * Called by the VM, during -Xshare:dump */ private void resetArchivedStates() { - pdcache.clear(); + if (CDS.isDumpingAOTLinkedClasses()) { + for (CodeSourceKey key : pdcache.keySet()) { + if (key.cs.getCodeSigners() != null) { + // We don't archive any signed classes, so we don't need to cache their ProtectionDomains. + pdcache.remove(key); + } + } + if (System.getProperty("cds.debug.archived.protection.domains") != null) { + for (CodeSourceKey key : pdcache.keySet()) { + System.out.println("Archiving ProtectionDomain " + key.cs + " for " + this); + } + } + } else { + pdcache.clear(); + } } } diff --git a/src/java.base/share/classes/java/security/Signature.java b/src/java.base/share/classes/java/security/Signature.java index 52aa4328b2c..228d6fff82b 100644 --- a/src/java.base/share/classes/java/security/Signature.java +++ b/src/java.base/share/classes/java/security/Signature.java @@ -36,14 +36,12 @@ import java.nio.ByteBuffer; import java.security.Provider.Service; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.BadPaddingException; -import javax.crypto.NoSuchPaddingException; import jdk.internal.access.JavaSecuritySignatureAccess; import jdk.internal.access.SharedSecrets; import sun.security.util.Debug; +import sun.security.util.CryptoAlgorithmConstraints; + import sun.security.jca.*; import sun.security.jca.GetInstance.Instance; import sun.security.util.KnownOIDs; @@ -213,20 +211,6 @@ public abstract class Signature extends SignatureSpi { this.algorithm = algorithm; } - // name of the special signature alg - private static final String RSA_SIGNATURE = "NONEwithRSA"; - - // name of the equivalent cipher alg - private static final String RSA_CIPHER = "RSA/ECB/PKCS1Padding"; - - // all the services we need to lookup for compatibility with Cipher - private static final List rsaIds = List.of( - new ServiceId("Signature", "NONEwithRSA"), - new ServiceId("Cipher", "RSA/ECB/PKCS1Padding"), - new ServiceId("Cipher", "RSA/ECB"), - new ServiceId("Cipher", "RSA//PKCS1Padding"), - new ServiceId("Cipher", "RSA")); - /** * Returns a {@code Signature} object that implements the specified * signature algorithm. @@ -241,12 +225,22 @@ public abstract class Signature extends SignatureSpi { * the {@link Security#getProviders() Security.getProviders()} method. * * @implNote - * The JDK Reference Implementation additionally uses the - * {@code jdk.security.provider.preferred} + * The JDK Reference Implementation additionally uses the following + * security properties: + *
      + *
    • the {@code jdk.security.provider.preferred} * {@link Security#getProperty(String) Security} property to determine * the preferred provider order for the specified algorithm. This * may be different from the order of providers returned by * {@link Security#getProviders() Security.getProviders()}. + *
    • + *
    • the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + *
    • + *
    * * @param algorithm the standard name of the algorithm requested. * See the Signature section in the
    t; - if (algorithm.equalsIgnoreCase(RSA_SIGNATURE)) { - t = GetInstance.getServices(rsaIds); - } else { - t = GetInstance.getServices("Signature", algorithm); + + if (!CryptoAlgorithmConstraints.permits("Signature", algorithm)) { + throw new NoSuchAlgorithmException(algorithm + " is disabled"); } + + Iterator t = GetInstance.getServices("Signature", algorithm); if (!t.hasNext()) { throw new NoSuchAlgorithmException (algorithm + " Signature not available"); @@ -329,10 +323,6 @@ public abstract class Signature extends SignatureSpi { } private static boolean isSpi(Service s) { - if (s.getType().equals("Cipher")) { - // must be a CipherSpi, which we can wrap with the CipherAdapter - return true; - } String className = s.getClassName(); Boolean result = signatureInfo.get(className); if (result == null) { @@ -370,6 +360,14 @@ public abstract class Signature extends SignatureSpi { *

    Note that the list of registered providers may be retrieved via * the {@link Security#getProviders() Security.getProviders()} method. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param algorithm the name of the algorithm requested. * See the Signature section in the @@ -398,18 +396,11 @@ public abstract class Signature extends SignatureSpi { public static Signature getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException { Objects.requireNonNull(algorithm, "null algorithm name"); - if (algorithm.equalsIgnoreCase(RSA_SIGNATURE)) { - // exception compatibility with existing code - if (provider == null || provider.isEmpty()) { - throw new IllegalArgumentException("missing provider"); - } - Provider p = Security.getProvider(provider); - if (p == null) { - throw new NoSuchProviderException - ("no such provider: " + provider); - } - return getInstanceRSA(p); + + if (!CryptoAlgorithmConstraints.permits("Signature", algorithm)) { + throw new NoSuchAlgorithmException(algorithm + " is disabled"); } + Instance instance = GetInstance.getInstance ("Signature", SignatureSpi.class, algorithm, provider); return getInstance(instance, algorithm); @@ -424,6 +415,14 @@ public abstract class Signature extends SignatureSpi { * is returned. Note that the specified provider does not * have to be registered in the provider list. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param algorithm the name of the algorithm requested. * See the Signature section in the @@ -450,40 +449,16 @@ public abstract class Signature extends SignatureSpi { public static Signature getInstance(String algorithm, Provider provider) throws NoSuchAlgorithmException { Objects.requireNonNull(algorithm, "null algorithm name"); - if (algorithm.equalsIgnoreCase(RSA_SIGNATURE)) { - // exception compatibility with existing code - if (provider == null) { - throw new IllegalArgumentException("missing provider"); - } - return getInstanceRSA(provider); + + if (!CryptoAlgorithmConstraints.permits("Signature", algorithm)) { + throw new NoSuchAlgorithmException(algorithm + " is disabled"); } + Instance instance = GetInstance.getInstance ("Signature", SignatureSpi.class, algorithm, provider); return getInstance(instance, algorithm); } - // return an implementation for NONEwithRSA, which is a special case - // because of the Cipher.RSA/ECB/PKCS1Padding compatibility wrapper - private static Signature getInstanceRSA(Provider p) - throws NoSuchAlgorithmException { - // try Signature first - Service s = p.getService("Signature", RSA_SIGNATURE); - if (s != null) { - Instance instance = GetInstance.getInstance(s, SignatureSpi.class); - return getInstance(instance, RSA_SIGNATURE); - } - // check Cipher - try { - Cipher c = Cipher.getInstance(RSA_CIPHER, p); - return Delegate.of(new CipherAdapter(c), RSA_SIGNATURE); - } catch (GeneralSecurityException e) { - // throw Signature style exception message to avoid confusion, - // but append Cipher exception as cause - throw new NoSuchAlgorithmException("no such algorithm: " - + RSA_SIGNATURE + " for provider " + p.getName(), e); - } - } - /** * Returns the provider of this {@code Signature} object. * @@ -1179,22 +1154,12 @@ public abstract class Signature extends SignatureSpi { private static SignatureSpi newInstance(Service s) throws NoSuchAlgorithmException { - if (s.getType().equals("Cipher")) { - // must be NONEwithRSA - try { - Cipher c = Cipher.getInstance(RSA_CIPHER, s.getProvider()); - return new CipherAdapter(c); - } catch (NoSuchPaddingException e) { - throw new NoSuchAlgorithmException(e); - } - } else { - Object o = s.newInstance(null); - if (!(o instanceof SignatureSpi)) { - throw new NoSuchAlgorithmException - ("Not a SignatureSpi: " + o.getClass().getName()); - } - return (SignatureSpi)o; + Object o = s.newInstance(null); + if (!(o instanceof SignatureSpi)) { + throw new NoSuchAlgorithmException + ("Not a SignatureSpi: " + o.getClass().getName()); } + return (SignatureSpi)o; } // max number of debug warnings to print from chooseFirstProvider() @@ -1471,92 +1436,4 @@ public abstract class Signature extends SignatureSpi { return sigSpi.engineGetParameters(); } } - - // adapter for RSA/ECB/PKCS1Padding ciphers - @SuppressWarnings("deprecation") - private static class CipherAdapter extends SignatureSpi { - - private final Cipher cipher; - - private ByteArrayOutputStream data; - - CipherAdapter(Cipher cipher) { - this.cipher = cipher; - } - - protected void engineInitVerify(PublicKey publicKey) - throws InvalidKeyException { - cipher.init(Cipher.DECRYPT_MODE, publicKey); - if (data == null) { - data = new ByteArrayOutputStream(128); - } else { - data.reset(); - } - } - - protected void engineInitSign(PrivateKey privateKey) - throws InvalidKeyException { - cipher.init(Cipher.ENCRYPT_MODE, privateKey); - data = null; - } - - protected void engineInitSign(PrivateKey privateKey, - SecureRandom random) throws InvalidKeyException { - cipher.init(Cipher.ENCRYPT_MODE, privateKey, random); - data = null; - } - - protected void engineUpdate(byte b) throws SignatureException { - engineUpdate(new byte[] {b}, 0, 1); - } - - protected void engineUpdate(byte[] b, int off, int len) - throws SignatureException { - if (data != null) { - data.write(b, off, len); - return; - } - byte[] out = cipher.update(b, off, len); - if ((out != null) && (out.length != 0)) { - throw new SignatureException - ("Cipher unexpectedly returned data"); - } - } - - protected byte[] engineSign() throws SignatureException { - try { - return cipher.doFinal(); - } catch (IllegalBlockSizeException | BadPaddingException e) { - throw new SignatureException("doFinal() failed", e); - } - } - - protected boolean engineVerify(byte[] sigBytes) - throws SignatureException { - try { - byte[] out = cipher.doFinal(sigBytes); - byte[] dataBytes = data.toByteArray(); - data.reset(); - return MessageDigest.isEqual(out, dataBytes); - } catch (BadPaddingException e) { - // e.g. wrong public key used - // return false rather than throwing exception - return false; - } catch (IllegalBlockSizeException e) { - throw new SignatureException("doFinal() failed", e); - } - } - - protected void engineSetParameter(String param, Object value) - throws InvalidParameterException { - throw new InvalidParameterException("Parameters not supported"); - } - - protected Object engineGetParameter(String param) - throws InvalidParameterException { - throw new InvalidParameterException("Parameters not supported"); - } - - } - } diff --git a/src/java.base/share/classes/java/text/Collator.java b/src/java.base/share/classes/java/text/Collator.java index 276a66cdc07..5e576cd800f 100644 --- a/src/java.base/share/classes/java/text/Collator.java +++ b/src/java.base/share/classes/java/text/Collator.java @@ -111,8 +111,13 @@ import sun.util.locale.provider.LocaleServiceProviderPool; *
    * @apiNote {@code CollationKey}s from different * {@code Collator}s can not be compared. See the class description - * for {@link CollationKey} - * for an example using {@code CollationKey}s. + * for {@link CollationKey} for an example using {@code CollationKey}s. + * + * @implNote Significant thread contention may occur during concurrent usage + * of the JDK Reference Implementation's {@link RuleBasedCollator}, which is the + * subtype returned by the default provider of the {@link #getInstance()} factory + * methods. As such, users should consider retrieving a separate instance for + * each thread when used in multithreaded environments. * * @see RuleBasedCollator * @see CollationKey diff --git a/src/java.base/share/classes/java/text/CompactNumberFormat.java b/src/java.base/share/classes/java/text/CompactNumberFormat.java index 7163b2dd63b..74c1eabe970 100644 --- a/src/java.base/share/classes/java/text/CompactNumberFormat.java +++ b/src/java.base/share/classes/java/text/CompactNumberFormat.java @@ -250,38 +250,43 @@ public final class CompactNumberFormat extends NumberFormat { /** * List of positive prefix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List positivePrefixPatterns; /** * List of negative prefix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List negativePrefixPatterns; /** * List of positive suffix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List positiveSuffixPatterns; /** * List of negative suffix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List negativeSuffixPatterns; /** * List of divisors of this formatter's compact number patterns. * Divisor can be either Long or BigInteger (if the divisor value goes - * beyond long boundary) + * beyond long boundary). This field is a read-only constant + * once initialized. */ private transient List divisors; /** * List of place holders that represent minimum integer digits at each index - * for each count. + * for each count. This field is a read-only constant once initialized. */ private transient List placeHolderPatterns; @@ -374,7 +379,7 @@ public final class CompactNumberFormat extends NumberFormat { /** * The map for plural rules that maps LDML defined tags (e.g. "one") to - * its rule. + * its rule. This field is a read-only constant once initialized. */ private transient Map rulesMap; @@ -1515,7 +1520,7 @@ public final class CompactNumberFormat extends NumberFormat { } } - private final transient DigitList digitList = new DigitList(); + private transient DigitList digitList = new DigitList(); private static final int STATUS_INFINITE = 0; private static final int STATUS_POSITIVE = 1; private static final int STATUS_LENGTH = 2; @@ -2506,8 +2511,14 @@ public final class CompactNumberFormat extends NumberFormat { @Override public CompactNumberFormat clone() { CompactNumberFormat other = (CompactNumberFormat) super.clone(); + + // Cloning reference fields. Other fields (e.g., "positivePrefixPatterns") + // are not cloned since they are read-only constants after initialization. other.compactPatterns = compactPatterns.clone(); other.symbols = (DecimalFormatSymbols) symbols.clone(); + other.decimalFormat = (DecimalFormat) decimalFormat.clone(); + other.defaultDecimalFormat = (DecimalFormat) defaultDecimalFormat.clone(); + other.digitList = (DigitList) digitList.clone(); return other; } diff --git a/src/java.base/share/classes/java/text/DecimalFormat.java b/src/java.base/share/classes/java/text/DecimalFormat.java index 7ace5e136fe..c803a97ad86 100644 --- a/src/java.base/share/classes/java/text/DecimalFormat.java +++ b/src/java.base/share/classes/java/text/DecimalFormat.java @@ -2335,9 +2335,10 @@ public class DecimalFormat extends NumberFormat { // (bug 4162852). if (multiplier != 1 && gotDouble) { longResult = (long)doubleResult; - gotDouble = ((doubleResult != (double)longResult) || - (doubleResult == 0.0 && 1/doubleResult < 0.0)) && - !isParseIntegerOnly(); + gotDouble = ((doubleResult >= Long.MAX_VALUE || doubleResult <= Long.MIN_VALUE) || + (doubleResult != (double)longResult) || + (doubleResult == 0.0 && 1/doubleResult < 0.0)) && + !isParseIntegerOnly(); } // cast inside of ?: because of binary numeric promotion, JLS 15.25 diff --git a/src/java.base/share/classes/java/text/DigitList.java b/src/java.base/share/classes/java/text/DigitList.java index 2895126f93b..9ad1203fb62 100644 --- a/src/java.base/share/classes/java/text/DigitList.java +++ b/src/java.base/share/classes/java/text/DigitList.java @@ -46,6 +46,7 @@ import java.nio.charset.StandardCharsets; import jdk.internal.access.SharedSecrets; import jdk.internal.math.FloatingDecimal; import jdk.internal.util.ArraysSupport; +import jdk.internal.vm.annotation.Stable; /** * Digit List. Private to DecimalFormat. @@ -108,7 +109,6 @@ final class DigitList implements Cloneable { public int count = 0; public byte[] digits = new byte[MAX_COUNT]; - private byte[] data; private RoundingMode roundingMode = RoundingMode.HALF_EVEN; private boolean isNegative = false; @@ -320,15 +320,18 @@ final class DigitList implements Cloneable { * fractional digits to be converted. If false, total digits. */ void set(boolean isNegative, double source, int maximumDigits, boolean fixedPoint) { + assert Double.isFinite(source); + FloatingDecimal.BinaryToASCIIConverter fdConverter = FloatingDecimal.getBinaryToASCIIConverter(source, COMPAT); boolean hasBeenRoundedUp = fdConverter.digitsRoundedUp(); boolean valueExactAsDecimal = fdConverter.decimalDigitsExact(); - assert !fdConverter.isExceptional(); - byte[] chars = getDataChars(26); - int len = fdConverter.getChars(chars); - set(isNegative, chars, len, + count = fdConverter.getDigits(digits); + + int exp = fdConverter.getDecimalExponent() - count; + + set(isNegative, exp, hasBeenRoundedUp, valueExactAsDecimal, maximumDigits, fixedPoint); } @@ -340,43 +343,22 @@ final class DigitList implements Cloneable { * @param valueExactAsDecimal whether or not collected digits provide * an exact decimal representation of the value. */ - private void set(boolean isNegative, byte[] source, int len, + private void set(boolean isNegative, int exp, boolean roundedUp, boolean valueExactAsDecimal, int maximumDigits, boolean fixedPoint) { this.isNegative = isNegative; - decimalAt = -1; - count = 0; - int exponent = 0; - // Number of zeros between decimal point and first non-zero digit after - // decimal point, for numbers < 1. - int leadingZerosAfterDecimal = 0; - boolean nonZeroDigitSeen = false; + if (!nonZeroAfterIndex(0)) { + count = 0; + decimalAt = 0; + return; + } + decimalAt = count + exp; - for (int i = 0; i < len; ) { - byte c = source[i++]; - if (c == '.') { - decimalAt = count; - } else if (c == 'e' || c == 'E') { - exponent = parseInt(source, i, len); - break; - } else { - if (!nonZeroDigitSeen) { - nonZeroDigitSeen = (c != '0'); - if (!nonZeroDigitSeen && decimalAt != -1) - ++leadingZerosAfterDecimal; - } - if (nonZeroDigitSeen) { - digits[count++] = c; - } - } - } - if (decimalAt == -1) { - decimalAt = count; - } - if (nonZeroDigitSeen) { - decimalAt += exponent - leadingZerosAfterDecimal; + // Eliminate trailing zeros. + while (count > 1 && digits[count - 1] == '0') { + --count; } if (fixedPoint) { @@ -405,11 +387,6 @@ final class DigitList implements Cloneable { // else fall through } - // Eliminate trailing zeros. - while (count > 1 && digits[count - 1] == '0') { - --count; - } - // Eliminate digits beyond maximum digits to be displayed. // Round up if appropriate. round(fixedPoint ? (maximumDigits + decimalAt) : maximumDigits, @@ -669,13 +646,13 @@ final class DigitList implements Cloneable { */ @SuppressWarnings("deprecation") void set(boolean isNegative, BigDecimal source, int maximumDigits, boolean fixedPoint) { - String s = source.toString(); - extendDigits(s.length()); - + String s = source.unscaledValue().toString(); int len = s.length(); - byte[] chars = getDataChars(len); - s.getBytes(0, len, chars, 0); - set(isNegative, chars, len, + + extendDigits(len); + s.getBytes(0, len, digits, 0); + count = len; + set(isNegative, -source.scale(), false, true, maximumDigits, fixedPoint); } @@ -745,14 +722,7 @@ final class DigitList implements Cloneable { public Object clone() { try { DigitList other = (DigitList) super.clone(); - byte[] newDigits = new byte[digits.length]; - System.arraycopy(digits, 0, newDigits, 0, digits.length); - other.digits = newDigits; - - // Data does not need to be copied because it does - // not carry significant information. It will be recreated on demand. - // Setting it to null is needed to avoid sharing across clones. - other.data = null; + other.digits = digits.clone(); return other; } catch (CloneNotSupportedException e) { @@ -760,29 +730,8 @@ final class DigitList implements Cloneable { } } - private static int parseInt(byte[] str, int offset, int strLen) { - byte c; - boolean positive = true; - if ((c = str[offset]) == '-') { - positive = false; - offset++; - } else if (c == '+') { - offset++; - } - - int value = 0; - while (offset < strLen) { - c = str[offset++]; - if (c >= '0' && c <= '9') { - value = value * 10 + (c - '0'); - } else { - break; - } - } - return positive ? value : -value; - } - // The digit part of -9223372036854775808L + @Stable private static final byte[] LONG_MIN_REP = "9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); public String toString() { @@ -798,11 +747,4 @@ final class DigitList implements Cloneable { digits = new byte[len]; } } - - private byte[] getDataChars(int length) { - if (data == null || data.length < length) { - data = new byte[length]; - } - return data; - } } diff --git a/src/java.base/share/classes/java/text/RuleBasedCollator.java b/src/java.base/share/classes/java/text/RuleBasedCollator.java index dc45dafb846..af1b6b62bdf 100644 --- a/src/java.base/share/classes/java/text/RuleBasedCollator.java +++ b/src/java.base/share/classes/java/text/RuleBasedCollator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,10 +38,6 @@ package java.text; -import java.text.Normalizer; -import java.util.Vector; -import java.util.Locale; - /** * The {@code RuleBasedCollator} class is a concrete subclass of * {@code Collator} that provides a simple, data-driven, table @@ -239,6 +235,11 @@ import java.util.Locale; * * * + * @implNote For this implementation, concurrent usage of this class may + * lead to significant thread contention since {@code synchronized} is employed + * to ensure thread-safety. As such, users of this class should consider creating + * a separate instance for each thread when used in multithreaded environments. + * * @see Collator * @see CollationElementIterator * @author Helena Shih, Laura Werner, Richard Gillam diff --git a/src/java.base/share/classes/java/time/Duration.java b/src/java.base/share/classes/java/time/Duration.java index 88d49fa9e45..23577a8a634 100644 --- a/src/java.base/share/classes/java/time/Duration.java +++ b/src/java.base/share/classes/java/time/Duration.java @@ -172,7 +172,7 @@ public final class Duration * Obtains a {@code Duration} representing a number of standard 24 hour days. *

    * The seconds are calculated based on the standard definition of a day, - * where each day is 86400 seconds which implies a 24 hour day. + * where each day is 86,400 seconds which implies a 24 hour day. * The nanosecond in second field is set to zero. * * @param days the number of days, positive or negative @@ -187,7 +187,7 @@ public final class Duration * Obtains a {@code Duration} representing a number of standard hours. *

    * The seconds are calculated based on the standard definition of an hour, - * where each hour is 3600 seconds. + * where each hour is 3,600 seconds. * The nanosecond in second field is set to zero. * * @param hours the number of hours, positive or negative @@ -375,8 +375,8 @@ public final class Duration *

          *    "PT20.345S" -- parses as "20.345 seconds"
          *    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
    -     *    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
    -     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
    +     *    "PT10H"     -- parses as "10 hours" (where an hour is 3,600 seconds)
    +     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86,400 seconds)
          *    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
          *    "PT-6H3M"    -- parses as "-6 hours and +3 minutes"
          *    "-PT6H3M"    -- parses as "-6 hours and -3 minutes"
    @@ -477,7 +477,7 @@ public final class Duration
          * {@link ChronoField#NANO_OF_SECOND NANO_OF_SECOND} field should be supported.
          * 

    * The result of this method can be a negative duration if the end is before the start. - * To guarantee to obtain a positive duration call {@link #abs()} on the result. + * To guarantee a positive duration, call {@link #abs()} on the result. * * @param startInclusive the start instant, inclusive, not null * @param endExclusive the end instant, exclusive, not null @@ -752,7 +752,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in standard 24 hour days added. *

    - * The number of days is multiplied by 86400 to obtain the number of seconds to add. + * The number of days is multiplied by 86,400 to obtain the number of seconds to add. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -893,7 +893,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in standard 24 hour days subtracted. *

    - * The number of days is multiplied by 86400 to obtain the number of seconds to subtract. + * The number of days is multiplied by 86,400 to obtain the number of seconds to subtract. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -909,7 +909,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in hours subtracted. *

    - * The number of hours is multiplied by 3600 to obtain the number of seconds to subtract. + * The number of hours is multiplied by 3,600 to obtain the number of seconds to subtract. *

    * This instance is immutable and unaffected by this method call. * @@ -924,7 +924,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in minutes subtracted. *

    - * The number of hours is multiplied by 60 to obtain the number of seconds to subtract. + * The number of minutes is multiplied by 60 to obtain the number of seconds to subtract. *

    * This instance is immutable and unaffected by this method call. * @@ -1165,7 +1165,7 @@ public final class Duration * Gets the number of days in this duration. *

    * This returns the total number of days in the duration by dividing the - * number of seconds by 86400. + * number of seconds by 86,400. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -1180,7 +1180,7 @@ public final class Duration * Gets the number of hours in this duration. *

    * This returns the total number of hours in the duration by dividing the - * number of seconds by 3600. + * number of seconds by 3,600. *

    * This instance is immutable and unaffected by this method call. * @@ -1272,7 +1272,7 @@ public final class Duration * Extracts the number of days in the duration. *

    * This returns the total number of days in the duration by dividing the - * number of seconds by 86400. + * number of seconds by 86,400. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -1476,10 +1476,10 @@ public final class Duration *

    * Examples: *

    -     *    "20.345 seconds"                 -- "PT20.345S
    +     *    "20.345 seconds"                 -- "PT20.345S"
          *    "15 minutes" (15 * 60 seconds)   -- "PT15M"
    -     *    "10 hours" (10 * 3600 seconds)   -- "PT10H"
    -     *    "2 days" (2 * 86400 seconds)     -- "PT48H"
    +     *    "10 hours" (10 * 3,600 seconds)  -- "PT10H"
    +     *    "2 days" (2 * 86,400 seconds)    -- "PT48H"
          * 
    * Note that multiples of 24 hours are not output as days to avoid confusion * with {@code Period}. diff --git a/src/java.base/share/classes/java/time/Instant.java b/src/java.base/share/classes/java/time/Instant.java index 1c7a41d7f0e..db8db4aaa38 100644 --- a/src/java.base/share/classes/java/time/Instant.java +++ b/src/java.base/share/classes/java/time/Instant.java @@ -114,15 +114,15 @@ import java.util.Objects; *

    * The length of the solar day is the standard way that humans measure time. * This has traditionally been subdivided into 24 hours of 60 minutes of 60 seconds, - * forming a 86400 second day. + * forming an 86,400 second day. *

    * Modern timekeeping is based on atomic clocks which precisely define an SI second * relative to the transitions of a Caesium atom. The length of an SI second was defined - * to be very close to the 86400th fraction of a day. + * to be very close to the 86,400th fraction of a day. *

    * Unfortunately, as the Earth rotates the length of the day varies. * In addition, over time the average length of the day is getting longer as the Earth slows. - * As a result, the length of a solar day in 2012 is slightly longer than 86400 SI seconds. + * As a result, the length of a solar day in 2012 is slightly longer than 86,400 SI seconds. * The actual length of any given day and the amount by which the Earth is slowing * are not predictable and can only be determined by measurement. * The UT1 time-scale captures the accurate length of day, but is only available some @@ -131,7 +131,7 @@ import java.util.Objects; * The UTC time-scale is a standard approach to bundle up all the additional fractions * of a second from UT1 into whole seconds, known as leap-seconds. * A leap-second may be added or removed depending on the Earth's rotational changes. - * As such, UTC permits a day to have 86399 SI seconds or 86401 SI seconds where + * As such, UTC permits a day to have 86,399 SI seconds or 86,401 SI seconds where * necessary in order to keep the day aligned with the Sun. *

    * The modern UTC time-scale was introduced in 1972, introducing the concept of whole leap-seconds. @@ -143,7 +143,7 @@ import java.util.Objects; * Given the complexity of accurate timekeeping described above, this Java API defines * its own time-scale, the Java Time-Scale. *

    - * The Java Time-Scale divides each calendar day into exactly 86400 + * The Java Time-Scale divides each calendar day into exactly 86,400 * subdivisions, known as seconds. These seconds may differ from the * SI second. It closely matches the de facto international civil time * scale, the definition of which changes from time to time. @@ -171,7 +171,7 @@ import java.util.Objects; * This is identical to UTC on days that do not have a leap second. * On days that do have a leap second, the leap second is spread equally * over the last 1000 seconds of the day, maintaining the appearance of - * exactly 86400 seconds per day. + * exactly 86,400 seconds per day. *

    * For the segment prior to 1972-11-03, extending back arbitrarily far, * the consensus international time scale is defined to be UT1, applied diff --git a/src/java.base/share/classes/java/time/LocalDate.java b/src/java.base/share/classes/java/time/LocalDate.java index 6724410da2b..016bdab5394 100644 --- a/src/java.base/share/classes/java/time/LocalDate.java +++ b/src/java.base/share/classes/java/time/LocalDate.java @@ -182,11 +182,11 @@ public final class LocalDate /** * @serial The month-of-year. */ - private final byte month; + private final short month; /** * @serial The day-of-month. */ - private final byte day; + private final short day; //----------------------------------------------------------------------- /** @@ -490,8 +490,8 @@ public final class LocalDate */ private LocalDate(int year, int month, int dayOfMonth) { this.year = year; - this.month = (byte) month; - this.day = (byte) dayOfMonth; + this.month = (short) month; + this.day = (short) dayOfMonth; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/MonthDay.java b/src/java.base/share/classes/java/time/MonthDay.java index 6244c14e6e1..1de4fa84d3e 100644 --- a/src/java.base/share/classes/java/time/MonthDay.java +++ b/src/java.base/share/classes/java/time/MonthDay.java @@ -146,11 +146,11 @@ public final class MonthDay /** * @serial The month-of-year, not null. */ - private final byte month; + private final int month; /** * @serial The day-of-month. */ - private final byte day; + private final int day; //----------------------------------------------------------------------- /** @@ -319,8 +319,8 @@ public final class MonthDay * @param dayOfMonth the day-of-month to represent, validated from 1 to 29-31 */ private MonthDay(int month, int dayOfMonth) { - this.month = (byte) month; - this.day = (byte) dayOfMonth; + this.month = month; + this.day = dayOfMonth; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/Period.java b/src/java.base/share/classes/java/time/Period.java index 5ee80710edb..745714788c3 100644 --- a/src/java.base/share/classes/java/time/Period.java +++ b/src/java.base/share/classes/java/time/Period.java @@ -765,7 +765,7 @@ public final class Period *

    * This instance is immutable and unaffected by this method call. * - * @param daysToSubtract the months to subtract, positive or negative + * @param daysToSubtract the days to subtract, positive or negative * @return a {@code Period} based on this period with the specified days subtracted, not null * @throws ArithmeticException if numeric overflow occurs */ diff --git a/src/java.base/share/classes/java/time/YearMonth.java b/src/java.base/share/classes/java/time/YearMonth.java index b24151de3f0..8ad1172811f 100644 --- a/src/java.base/share/classes/java/time/YearMonth.java +++ b/src/java.base/share/classes/java/time/YearMonth.java @@ -153,7 +153,7 @@ public final class YearMonth /** * @serial The month-of-year, not null. */ - private final byte month; + private final int month; //----------------------------------------------------------------------- /** @@ -306,7 +306,7 @@ public final class YearMonth */ private YearMonth(int year, int month) { this.year = year; - this.month = (byte) month; + this.month = month; } /** diff --git a/src/java.base/share/classes/java/time/ZoneOffset.java b/src/java.base/share/classes/java/time/ZoneOffset.java index d93c6e2d46d..4199d17735c 100644 --- a/src/java.base/share/classes/java/time/ZoneOffset.java +++ b/src/java.base/share/classes/java/time/ZoneOffset.java @@ -417,9 +417,9 @@ public final class ZoneOffset /** * Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds *

    - * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800. + * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64,800 to +64,800. * - * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 + * @param totalSeconds the total time-zone offset in seconds, from -64,800 to +64,800 * @return the ZoneOffset, not null * @throws DateTimeException if the offset is not in the required range */ @@ -450,7 +450,7 @@ public final class ZoneOffset /** * Constructor. * - * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 + * @param totalSeconds the total time-zone offset in seconds, from -64,800 to +64,800 */ private ZoneOffset(int totalSeconds) { this.totalSeconds = totalSeconds; diff --git a/src/java.base/share/classes/java/time/ZonedDateTime.java b/src/java.base/share/classes/java/time/ZonedDateTime.java index 962469b3225..57dc98d5c68 100644 --- a/src/java.base/share/classes/java/time/ZonedDateTime.java +++ b/src/java.base/share/classes/java/time/ZonedDateTime.java @@ -136,7 +136,7 @@ import jdk.internal.util.DateTimeHelper; * For Overlaps, the general strategy is that if the local date-time falls in the * middle of an Overlap, then the previous offset will be retained. If there is no * previous offset, or the previous offset is invalid, then the earlier offset is - * used, typically "summer" time.. Two additional methods, + * used, typically "summer" time. Two additional methods, * {@link #withEarlierOffsetAtOverlap()} and {@link #withLaterOffsetAtOverlap()}, * help manage the case of an overlap. *

    @@ -246,7 +246,7 @@ public final class ZonedDateTime * Time-zone rules, such as daylight savings, mean that not every local date-time * is valid for the specified zone, thus the local date-time may be adjusted. *

    - * The local date time and first combined to form a local date-time. + * The local date and time are first combined to form a local date-time. * The local date-time is then resolved to a single instant on the time-line. * This is achieved by finding a valid offset from UTC/Greenwich for the local * date-time as defined by the {@link ZoneRules rules} of the zone ID. @@ -263,7 +263,7 @@ public final class ZonedDateTime * @param date the local date, not null * @param time the local time, not null * @param zone the time-zone, not null - * @return the offset date-time, not null + * @return the zoned date-time, not null */ public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) { return of(LocalDateTime.of(date, time), zone); @@ -333,7 +333,7 @@ public final class ZonedDateTime * @param second the second-of-minute to represent, from 0 to 59 * @param nanoOfSecond the nano-of-second to represent, from 0 to 999,999,999 * @param zone the time-zone, not null - * @return the offset date-time, not null + * @return the zoned date-time, not null * @throws DateTimeException if the value of any field is out of range, or * if the day-of-month is invalid for the month-year */ diff --git a/src/java.base/share/classes/java/time/chrono/HijrahDate.java b/src/java.base/share/classes/java/time/chrono/HijrahDate.java index 2d3e4f93e69..114a47e4797 100644 --- a/src/java.base/share/classes/java/time/chrono/HijrahDate.java +++ b/src/java.base/share/classes/java/time/chrono/HijrahDate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -137,11 +137,11 @@ public final class HijrahDate /** * The month-of-year. */ - private final transient byte monthOfYear; + private final transient int monthOfYear; /** * The day-of-month. */ - private final transient byte dayOfMonth; + private final transient int dayOfMonth; //------------------------------------------------------------------------- /** @@ -273,8 +273,8 @@ public final class HijrahDate this.chrono = chrono; this.prolepticYear = prolepticYear; - this.monthOfYear = (byte) monthOfYear; - this.dayOfMonth = (byte) dayOfMonth; + this.monthOfYear = monthOfYear; + this.dayOfMonth = dayOfMonth; } /** @@ -287,8 +287,8 @@ public final class HijrahDate this.chrono = chrono; this.prolepticYear = dateInfo[0]; - this.monthOfYear = (byte) dateInfo[1]; - this.dayOfMonth = (byte) dateInfo[2]; + this.monthOfYear = dateInfo[1]; + this.dayOfMonth = dateInfo[2]; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index 7a2142e5113..9f5b82775b9 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -1937,7 +1937,7 @@ public final class DateTimeFormatterBuilder { padNext(pad); // pad and continue parsing } // main rules - TemporalField field = FIELD_MAP.get(cur); + TemporalField field = getField(cur); if (field != null) { parseField(cur, count, field); } else if (cur == 'z') { @@ -2185,48 +2185,55 @@ public final class DateTimeFormatterBuilder { } } - /** Map of letters to fields. */ - private static final Map FIELD_MAP = new HashMap<>(); - static { + /** + * Returns the TemporalField for the given pattern character. + * + * @param ch the pattern character + * @return the TemporalField for the given pattern character, or null if not applicable + */ + private static TemporalField getField(char ch) { // SDF = SimpleDateFormat - FIELD_MAP.put('G', ChronoField.ERA); // SDF, LDML (different to both for 1/2 chars) - FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // SDF, LDML - FIELD_MAP.put('u', ChronoField.YEAR); // LDML (different in SDF) - FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310) - FIELD_MAP.put('q', IsoFields.QUARTER_OF_YEAR); // LDML (stand-alone) - FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // SDF, LDML - FIELD_MAP.put('L', ChronoField.MONTH_OF_YEAR); // SDF, LDML (stand-alone) - FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // SDF, LDML - FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // SDF, LDML - FIELD_MAP.put('F', ChronoField.ALIGNED_WEEK_OF_MONTH); // SDF, LDML - FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // SDF, LDML (different to both for 1/2 chars) - FIELD_MAP.put('c', ChronoField.DAY_OF_WEEK); // LDML (stand-alone) - FIELD_MAP.put('e', ChronoField.DAY_OF_WEEK); // LDML (needs localized week number) - FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // SDF, LDML - FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // SDF, LDML - FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // SDF, LDML - FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // SDF, LDML - FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // SDF, LDML - FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // SDF, LDML - FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // SDF, LDML - FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (SDF uses milli-of-second number) - FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML - FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML) - FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML) - FIELD_MAP.put('g', JulianFields.MODIFIED_JULIAN_DAY); - // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 - // 310 - Z - matches SimpleDateFormat and LDML - // 310 - V - time-zone id, matches LDML - // 310 - v - general timezone names, not matching exactly with LDML because LDML specify to fall back - // to 'VVVV' if general-nonlocation unavailable but here it's not falling back because of lack of data - // 310 - p - prefix for padding - // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 - // 310 - x - matches LDML - // 310 - w, W, and Y are localized forms matching LDML - // LDML - B - day periods - // LDML - U - cycle year name, not supported by 310 yet - // LDML - l - deprecated - // LDML - j - not relevant + return switch (ch) { + case 'G' -> ChronoField.ERA; // SDF, LDML (different to both for 1/2 chars) + case 'y' -> ChronoField.YEAR_OF_ERA; // SDF, LDML + case 'u' -> ChronoField.YEAR; // LDML (different in SDF) + case 'Q' -> IsoFields.QUARTER_OF_YEAR; // LDML (removed quarter from 310) + case 'q' -> IsoFields.QUARTER_OF_YEAR; // LDML (stand-alone) + case 'M' -> ChronoField.MONTH_OF_YEAR; // SDF, LDML + case 'L' -> ChronoField.MONTH_OF_YEAR; // SDF, LDML (stand-alone) + case 'D' -> ChronoField.DAY_OF_YEAR; // SDF, LDML + case 'd' -> ChronoField.DAY_OF_MONTH; // SDF, LDML + case 'F' -> ChronoField.ALIGNED_WEEK_OF_MONTH; // SDF, LDML + case 'E' -> ChronoField.DAY_OF_WEEK; // SDF, LDML (different to both for 1/2 chars) + case 'c' -> ChronoField.DAY_OF_WEEK; // LDML (stand-alone) + case 'e' -> ChronoField.DAY_OF_WEEK; // LDML (needs localized week number) + case 'a' -> ChronoField.AMPM_OF_DAY; // SDF, LDML + case 'H' -> ChronoField.HOUR_OF_DAY; // SDF, LDML + case 'k' -> ChronoField.CLOCK_HOUR_OF_DAY; // SDF, LDML + case 'K' -> ChronoField.HOUR_OF_AMPM; // SDF, LDML + case 'h' -> ChronoField.CLOCK_HOUR_OF_AMPM; // SDF, LDML + case 'm' -> ChronoField.MINUTE_OF_HOUR; // SDF, LDML + case 's' -> ChronoField.SECOND_OF_MINUTE; // SDF, LDML + case 'S' -> ChronoField.NANO_OF_SECOND; // LDML (SDF uses milli-of-second number) + case 'A' -> ChronoField.MILLI_OF_DAY; // LDML + case 'n' -> ChronoField.NANO_OF_SECOND; // 310 (proposed for LDML) + case 'N' -> ChronoField.NANO_OF_DAY; // 310 (proposed for LDML) + case 'g' -> JulianFields.MODIFIED_JULIAN_DAY; + default -> null; + // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 + // 310 - Z - matches SimpleDateFormat and LDML + // 310 - V - time-zone id, matches LDML + // 310 - v - general timezone names, not matching exactly with LDML because LDML specify to fall back + // to 'VVVV' if general-nonlocation unavailable but here it's not falling back because of lack of data + // 310 - p - prefix for padding + // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 + // 310 - x - matches LDML + // 310 - w, W, and Y are localized forms matching LDML + // LDML - B - day periods + // LDML - U - cycle year name, not supported by 310 yet + // LDML - l - deprecated + // LDML - j - not relevant + }; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/package-info.java b/src/java.base/share/classes/java/time/package-info.java index 8a4fbe44d8b..d0fb7a6c2bc 100644 --- a/src/java.base/share/classes/java/time/package-info.java +++ b/src/java.base/share/classes/java/time/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -65,7 +65,7 @@ * The main API for dates, times, instants, and durations. *

    *

    - * The classes defined here represent the principle date-time concepts, + * The classes defined here represent the principal date-time concepts, * including instants, durations, dates, times, time-zones and periods. * They are based on the ISO calendar system, which is the de facto world * calendar following the proleptic Gregorian rules. @@ -150,7 +150,7 @@ *

    *

    * {@link java.time.OffsetTime} stores a time and offset from UTC without a date. - * This stores a date like '11:30+01:00'. + * This stores a time like '11:30+01:00'. * The {@link java.time.ZoneOffset ZoneOffset} is of the form '+01:00'. *

    *

    @@ -249,7 +249,7 @@ *

    * Multiple calendar systems is an awkward addition to the design challenges. * The first principle is that most users want the standard ISO calendar system. - * As such, the main classes are ISO-only. The second principle is that most of those that want a + * As such, the main classes are ISO-only. The second principle is that most of those who want a * non-ISO calendar system want it for user interaction, thus it is a UI localization issue. * As such, date and time objects should be held as ISO objects in the data model and persistent * storage, only being converted to and from a local calendar for display. diff --git a/src/java.base/share/classes/java/time/temporal/ChronoField.java b/src/java.base/share/classes/java/time/temporal/ChronoField.java index 9e5de367b0e..506737ff268 100644 --- a/src/java.base/share/classes/java/time/temporal/ChronoField.java +++ b/src/java.base/share/classes/java/time/temporal/ChronoField.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -599,7 +599,7 @@ public enum ChronoField implements TemporalField { * A {@link ZoneOffset} represents the period of time that local time differs from UTC/Greenwich. * This is usually a fixed number of hours and minutes. * It is equivalent to the {@link ZoneOffset#getTotalSeconds() total amount} of the offset in seconds. - * For example, during the winter Paris has an offset of {@code +01:00}, which is 3600 seconds. + * For example, during the winter, Paris has an offset of {@code +01:00}, which is 3,600 seconds. *

    * This field is strictly defined to have the same meaning in all calendar systems. * This is necessary to ensure interoperation between calendars. diff --git a/src/java.base/share/classes/java/time/temporal/ValueRange.java b/src/java.base/share/classes/java/time/temporal/ValueRange.java index 442cf0a2509..27e71e77d66 100644 --- a/src/java.base/share/classes/java/time/temporal/ValueRange.java +++ b/src/java.base/share/classes/java/time/temporal/ValueRange.java @@ -78,7 +78,7 @@ import java.time.DateTimeException; * Only the minimum and maximum values are provided. * It is possible for there to be invalid values within the outer range. * For example, a weird field may have valid values of 1, 2, 4, 6, 7, thus - * have a range of '1 - 7', despite that fact that values 3 and 5 are invalid. + * have a range of '1 - 7', despite the fact that values 3 and 5 are invalid. *

    * Instances of this class are not tied to a specific field. * diff --git a/src/java.base/share/classes/java/util/Comparator.java b/src/java.base/share/classes/java/util/Comparator.java index 6e0420d26e8..ad48dc94ed6 100644 --- a/src/java.base/share/classes/java/util/Comparator.java +++ b/src/java.base/share/classes/java/util/Comparator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,7 +30,6 @@ import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; import java.util.function.ToDoubleFunction; -import java.util.Comparators; /** * A comparison function, which imposes a total ordering on @@ -189,6 +188,52 @@ public interface Comparator { return Collections.reverseOrder(this); } + /** + * Returns the greater of two values according to this comparator. + * If the arguments are equal with respect to this comparator, + * the {@code o1} argument is returned. + * + * @implSpec This default implementation behaves as if + * {@code compare(o1, o2) >= 0 ? o1 : o2}. + * + * @param o1 an argument. + * @param o2 another argument. + * @param the type of the arguments and the result. + * @return the larger of {@code o1} and {@code o2} according to this comparator. + * @throws NullPointerException if an argument is null and this + * comparator does not permit null arguments + * @throws ClassCastException if the arguments' types prevent them from + * being compared by this comparator. + * + * @since 26 + */ + default U max(U o1, U o2) { + return compare(o1, o2) >= 0 ? o1 : o2; + } + + /** + * Returns the smaller of two values according to this comparator. + * If the arguments are equal with respect to this comparator, + * the {@code o1} argument is returned. + * + * @implSpec This default implementation behaves as if + * {@code compare(o1, o2) <= 0 ? o1 : o2}. + * + * @param o1 an argument. + * @param o2 another argument. + * @param the type of the arguments and the result. + * @return the smaller of {@code o1} and {@code o2} according to this comparator. + * @throws NullPointerException if an argument is null and this + * comparator does not permit null arguments + * @throws ClassCastException if the arguments' types prevent them from + * being compared by this comparator. + * + * @since 26 + */ + default U min(U o1, U o2) { + return compare(o1, o2) <= 0 ? o1 : o2; + } + /** * Returns a lexicographic-order comparator with another comparator. * If this {@code Comparator} considers two elements equal, i.e. diff --git a/src/java.base/share/classes/java/util/GregorianCalendar.java b/src/java.base/share/classes/java/util/GregorianCalendar.java index 9c75cbc5732..26b0c7c1f2f 100644 --- a/src/java.base/share/classes/java/util/GregorianCalendar.java +++ b/src/java.base/share/classes/java/util/GregorianCalendar.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1214,8 +1214,10 @@ public class GregorianCalendar extends Calendar { d.setHours(hourOfDay); time = calsys.getTime(d); - // If we stay on the same wall-clock time, try the next or previous hour. - if (internalGet(HOUR_OF_DAY) == d.getHours()) { + // If the rolled amount is not a full HOUR/HOUR_OF_DAY (12/24-hour) cycle and + // if we stay on the same wall-clock time, try the next or previous hour. + if (((field == HOUR_OF_DAY && amount % 24 != 0) || (field == HOUR && amount % 12 != 0)) + && internalGet(HOUR_OF_DAY) == d.getHours()) { hourOfDay = getRolledValue(rolledValue, amount > 0 ? +1 : -1, min, max); if (field == HOUR && internalGet(AM_PM) == PM) { hourOfDay += 12; diff --git a/src/java.base/share/classes/java/util/Locale.java b/src/java.base/share/classes/java/util/Locale.java index eb69d1dfc2f..a55ddee648e 100644 --- a/src/java.base/share/classes/java/util/Locale.java +++ b/src/java.base/share/classes/java/util/Locale.java @@ -2726,9 +2726,13 @@ public final class Locale implements Cloneable, Serializable { * @see Locale#forLanguageTag(String) */ public Builder setLanguageTag(String languageTag) { - LanguageTag tag = LanguageTag.parse( - languageTag, new ParsePosition(0), false); - localeBuilder.setLanguageTag(tag); + if (LocaleUtils.isEmpty(languageTag)) { + localeBuilder.clear(); + } else { + LanguageTag tag = LanguageTag.parse( + languageTag, new ParsePosition(0), false); + localeBuilder.setLanguageTag(tag); + } return this; } diff --git a/src/java.base/share/classes/java/util/SimpleTimeZone.java b/src/java.base/share/classes/java/util/SimpleTimeZone.java index 58d43692ee0..f047ce26e58 100644 --- a/src/java.base/share/classes/java/util/SimpleTimeZone.java +++ b/src/java.base/share/classes/java/util/SimpleTimeZone.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -112,15 +112,15 @@ import sun.util.calendar.Gregorian; *

    
      *      // Base GMT offset: -8:00
      *      // DST starts:      at 2:00am in standard time
    - *      //                  on the first Sunday in April
    + *      //                  on the second Sunday in March
      *      // DST ends:        at 2:00am in daylight time
    - *      //                  on the last Sunday in October
    + *      //                  on the first Sunday in November
      *      // Save:            1 hour
      *      SimpleTimeZone(-28800000,
      *                     "America/Los_Angeles",
    - *                     Calendar.APRIL, 1, -Calendar.SUNDAY,
    + *                     Calendar.MARCH, 8, -Calendar.SUNDAY,
      *                     7200000,
    - *                     Calendar.OCTOBER, -1, Calendar.SUNDAY,
    + *                     Calendar.NOVEMBER, 1, -Calendar.SUNDAY,
      *                     7200000,
      *                     3600000)
      *
    @@ -863,13 +863,24 @@ public class SimpleTimeZone extends TimeZone {
         }
     
         /**
    -     * Generates the hash code for the SimpleDateFormat object.
    +     * Generates the hash code for the SimpleTimeZone object.
          * @return the hash code for this object
          */
         public int hashCode()
         {
    -        return startMonth ^ startDay ^ startDayOfWeek ^ startTime ^
    -            endMonth ^ endDay ^ endDayOfWeek ^ endTime ^ rawOffset;
    +        int hash = 31 * getID().hashCode() + rawOffset;
    +        hash = 31 * hash + Boolean.hashCode(useDaylight);
    +        if (useDaylight) {
    +            hash = 31 * hash + startMonth;
    +            hash = 31 * hash + startDay;
    +            hash = 31 * hash + startDayOfWeek;
    +            hash = 31 * hash + startTime;
    +            hash = 31 * hash + endMonth;
    +            hash = 31 * hash + endDay;
    +            hash = 31 * hash + endDayOfWeek;
    +            hash = 31 * hash + endTime;
    +        }
    +        return hash;
         }
     
         /**
    diff --git a/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java b/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java
    index 7503c154ddb..1338f2fd804 100644
    --- a/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java
    +++ b/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java
    @@ -1904,8 +1904,8 @@ public class CompletableFuture implements Future, CompletionStage {
             while ((r = result) == null) {
                 if (q == null) {
                     q = new Signaller(interruptible, 0L, 0L);
    -                if (Thread.currentThread() instanceof ForkJoinWorkerThread)
    -                    ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q);
    +                if (Thread.currentThread() instanceof ForkJoinWorkerThread wt)
    +                    ForkJoinPool.helpAsyncBlocker(wt.pool, q);
                 }
                 else if (!queued)
                     queued = tryPushStack(q);
    @@ -1950,8 +1950,8 @@ public class CompletableFuture implements Future, CompletionStage {
                     break;
                 else if (q == null) {
                     q = new Signaller(true, nanos, deadline);
    -                if (Thread.currentThread() instanceof ForkJoinWorkerThread)
    -                    ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q);
    +                if (Thread.currentThread() instanceof ForkJoinWorkerThread wt)
    +                    ForkJoinPool.helpAsyncBlocker(wt.pool, q);
                 }
                 else if (!queued)
                     queued = tryPushStack(q);
    diff --git a/src/java.base/share/classes/java/util/concurrent/Executors.java b/src/java.base/share/classes/java/util/concurrent/Executors.java
    index ef3d6348010..8b339cf83ef 100644
    --- a/src/java.base/share/classes/java/util/concurrent/Executors.java
    +++ b/src/java.base/share/classes/java/util/concurrent/Executors.java
    @@ -755,6 +755,13 @@ public final class Executors {
                 super.shutdown();
                 cleanable.clean();  // unregisters the cleanable
             }
    +
    +        @Override
    +        public List shutdownNow() {
    +            List unexecuted = super.shutdownNow();
    +            cleanable.clean();  // unregisters the cleanable
    +            return unexecuted;
    +        }
         }
     
         /**
    diff --git a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java
    index 51b2488264e..137cac45ed0 100644
    --- a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java
    +++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java
    @@ -531,19 +531,22 @@ public abstract class ForkJoinTask implements Future, Serializable {
          * still correct, although it may contain a misleading stack
          * trace.
          *
    -     * @param asExecutionException true if wrap as ExecutionException
    +     * @param asExecutionException true if wrap the result as an
    +     * ExecutionException. This applies only to actual exceptions, not
    +     * implicit CancellationExceptions issued when not THROWN or
    +     * available, which are not wrapped because by default they are
    +     * issued separately from ExecutionExceptions by callers. Which
    +     * may require further handling when this is not true (currently
    +     * only in InvokeAnyTask).
          * @return the exception, or null if none
          */
         private Throwable getException(boolean asExecutionException) {
             int s; Throwable ex; Aux a;
             if ((s = status) >= 0 || (s & ABNORMAL) == 0)
                 return null;
    -        else if ((s & THROWN) == 0 || (a = aux) == null || (ex = a.ex) == null) {
    -            ex = new CancellationException();
    -            if (!asExecutionException || !(this instanceof InterruptibleTask))
    -                return ex;         // else wrap below
    -        }
    -        else if (a.thread != Thread.currentThread()) {
    +        if ((s & THROWN) == 0 || (a = aux) == null || (ex = a.ex) == null)
    +            return new CancellationException();
    +        if (a.thread != Thread.currentThread()) {
                 try {
                     Constructor noArgCtor = null, oneArgCtor = null;
                     for (Constructor c : ex.getClass().getConstructors()) {
    @@ -1814,6 +1817,8 @@ public abstract class ForkJoinTask implements Future, Serializable {
                                      (t = new InvokeAnyTask(c, this, t)));
                     }
                     return timed ? get(nanos, TimeUnit.NANOSECONDS) : get();
    +            } catch (CancellationException ce) {
    +                throw new ExecutionException(ce);
                 } finally {
                     for (; t != null; t = t.pred)
                         t.onRootCompletion();
    diff --git a/src/java.base/share/classes/javax/crypto/Cipher.java b/src/java.base/share/classes/javax/crypto/Cipher.java
    index 82a607a5553..f95917b5c86 100644
    --- a/src/java.base/share/classes/javax/crypto/Cipher.java
    +++ b/src/java.base/share/classes/javax/crypto/Cipher.java
    @@ -30,7 +30,6 @@ import java.util.concurrent.ConcurrentHashMap;
     import java.util.concurrent.ConcurrentMap;
     import java.util.regex.*;
     
    -
     import java.security.*;
     import java.security.Provider.Service;
     import java.security.spec.AlgorithmParameterSpec;
    @@ -46,6 +45,7 @@ import java.nio.ReadOnlyBufferException;
     import sun.security.util.Debug;
     import sun.security.jca.*;
     import sun.security.util.KnownOIDs;
    +import sun.security.util.CryptoAlgorithmConstraints;
     
     /**
      * This class provides the functionality of a cryptographic cipher for
    @@ -286,7 +286,32 @@ public class Cipher {
             this.lock = new Object();
         }
     
    -    private static final String SHA512TRUNCATED = "SHA512/2";
    +    // for special handling SHA-512/224, SHA-512/256, SHA512/224, SHA512/256
    +    static int indexOfRealSlash(String s, int fromIndex) {
    +        while (true) {
    +            int pos = s.indexOf('/', fromIndex);
    +            // 512/2
    +            if (pos > 3 && pos + 1 < s.length()
    +                    && s.charAt(pos - 3) == '5'
    +                    && s.charAt(pos - 2) == '1'
    +                    && s.charAt(pos - 1) == '2'
    +                    && s.charAt(pos + 1) == '2') {
    +                fromIndex = pos + 1;
    +                // see 512/2, find next
    +            } else {
    +                return pos;
    +            }
    +        }
    +    }
    +
    +    static String reqNonEmpty(String in, String msg)
    +            throws NoSuchAlgorithmException {
    +        in = in.trim();
    +        if (in.isEmpty()) {
    +            throw new NoSuchAlgorithmException(msg);
    +        }
    +        return in;
    +    }
     
         // Parse the specified cipher transformation for algorithm and the
         // optional mode and padding. If the transformation contains only
    @@ -297,6 +322,7 @@ public class Cipher {
             if (transformation == null) {
                 throw new NoSuchAlgorithmException("No transformation given");
             }
    +
             /*
              * Components of a cipher transformation:
              *
    @@ -304,42 +330,34 @@ public class Cipher {
              * 2) feedback component (e.g., CFB) - optional
              * 3) padding component (e.g., PKCS5Padding) - optional
              */
    -
    -        // check if the transformation contains algorithms with "/" in their
    -        // name which can cause the parsing logic to go wrong
    -        int sha512Idx = transformation.toUpperCase(Locale.ENGLISH)
    -                .indexOf(SHA512TRUNCATED);
    -        int startIdx = (sha512Idx == -1 ? 0 :
    -                sha512Idx + SHA512TRUNCATED.length());
    -        int endIdx = transformation.indexOf('/', startIdx);
    -
    -        boolean algorithmOnly = (endIdx == -1);
    -        String algo = (algorithmOnly ? transformation.trim() :
    -                transformation.substring(0, endIdx).trim());
    -        if (algo.isEmpty()) {
    -            throw new NoSuchAlgorithmException("Invalid transformation: " +
    -                                   "algorithm not specified-"
    -                                   + transformation);
    +        int endIdx = indexOfRealSlash(transformation, 0);
    +        if (endIdx == -1) { // algo only, done
    +            return new String[] { reqNonEmpty(transformation,
    +                        "Invalid transformation: algorithm not specified")
    +            };
             }
    -        if (algorithmOnly) { // done
    -            return new String[] { algo };
    +        // must be algo/mode/padding
    +        String algo = reqNonEmpty(transformation.substring(0, endIdx),
    +                    "Invalid transformation: algorithm not specified");
    +
    +        int startIdx = endIdx + 1;
    +        endIdx = indexOfRealSlash(transformation, startIdx);
    +        if (endIdx == -1) {
    +            throw new NoSuchAlgorithmException(
    +                    "Invalid transformation format: " + transformation);
    +        }
    +        String mode = reqNonEmpty(transformation.substring(startIdx,
    +                endIdx), "Invalid transformation: missing mode");
    +
    +        startIdx = endIdx + 1;
    +        endIdx = indexOfRealSlash(transformation, startIdx);
    +        if (endIdx == -1) {
    +            return new String[] { algo, mode,
    +                    reqNonEmpty(transformation.substring(startIdx),
    +                            "Invalid transformation: missing padding") };
             } else {
    -            // continue parsing mode and padding
    -            startIdx = endIdx+1;
    -            endIdx = transformation.indexOf('/', startIdx);
    -            if (endIdx == -1) {
    -                throw new NoSuchAlgorithmException("Invalid transformation"
    -                            + " format:" + transformation);
    -            }
    -            String mode = transformation.substring(startIdx, endIdx).trim();
    -            String padding = transformation.substring(endIdx+1).trim();
    -            // ensure mode and padding are specified
    -            if (mode.isEmpty() || padding.isEmpty()) {
    -                throw new NoSuchAlgorithmException("Invalid transformation: " +
    -                                   "missing mode and/or padding-"
    -                                   + transformation);
    -            }
    -            return new String[] { algo, mode, padding };
    +            throw new NoSuchAlgorithmException(
    +                    "Invalid transformation format: " + transformation);
             }
         }
     
    @@ -482,8 +500,10 @@ public class Cipher {
          * requirements of your application.
          *
          * @implNote
    -     * The JDK Reference Implementation additionally uses the
    -     * {@code jdk.security.provider.preferred}
    +     * The JDK Reference Implementation additionally uses the following
    +     * security properties:
    +     * 
      + *
    • the {@code jdk.security.provider.preferred} * {@link Security#getProperty(String) Security} property to determine * the preferred provider order for the specified algorithm. This * may be different than the order of providers returned by @@ -491,6 +511,14 @@ public class Cipher { * See also the Cipher Transformations section of the {@extLink * security_guide_jdk_providers JDK Providers} document for information * on the transformation defaults used by JDK providers. + *
    • + *
    • the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + *
    • + *
    * * @param transformation the name of the transformation, e.g., * AES/CBC/PKCS5Padding. @@ -504,12 +532,12 @@ public class Cipher { * transformation * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if no provider supports a {@code CipherSpi} - * implementation for the specified algorithm + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation is not found or + * is found but does not support the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * is found but does not support the padding scheme * * @see java.security.Provider */ @@ -519,6 +547,13 @@ public class Cipher { if ((transformation == null) || transformation.isEmpty()) { throw new NoSuchAlgorithmException("Null or empty transformation"); } + + // throws NoSuchAlgorithmException if java.security disables it + if (!CryptoAlgorithmConstraints.permits("Cipher", transformation)) { + throw new NoSuchAlgorithmException(transformation + + " is disabled"); + } + List transforms = getTransforms(transformation); List cipherServices = new ArrayList<>(transforms.size()); for (Transform transform : transforms) { @@ -555,8 +590,12 @@ public class Cipher { failure = e; } } + if (failure instanceof NoSuchPaddingException nspe) { + throw nspe; + } throw new NoSuchAlgorithmException - ("Cannot find any provider supporting " + transformation, failure); + ("Cannot find any provider supporting " + transformation, + failure); } /** @@ -564,8 +603,8 @@ public class Cipher { * transformation. * *

    A new {@code Cipher} object encapsulating the - * {@code CipherSpi} implementation from the specified provider - * is returned. The specified provider must be registered + * {@code CipherSpi} implementation from the specified {@code provider} + * is returned. The specified {@code provider} must be registered * in the security provider list. * *

    Note that the list of registered providers may be retrieved via @@ -582,6 +621,14 @@ public class Cipher { * security_guide_jdk_providers JDK Providers} document for information * on the transformation defaults used by JDK providers. * + * @implNote + * The JDK Reference Implementation additionally uses + * the {@code jdk.crypto.disabledAlgorithms} + * {@link Security#getProperty(String) Security} property to determine + * if the specified algorithm is allowed. If the + * {@systemProperty jdk.crypto.disabledAlgorithms} is set, it supersedes + * the security property value. + * * @param transformation the name of the transformation, * e.g., AES/CBC/PKCS5Padding. * See the Cipher section in the AES/CBC/PKCS5Padding. * See the Cipher section in the transforms = getTransforms(transformation); boolean providerChecked = false; diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index e529e8ba350..fa6e5b4aac3 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -452,21 +452,6 @@ public interface JavaLangAccess { */ MethodHandle stringConcatHelper(String name, MethodType methodType); - /** - * Get the string concat initial coder - */ - long stringConcatInitialCoder(); - - /** - * Update lengthCoder for constant - */ - long stringConcatMix(long lengthCoder, String constant); - - /** - * Mix value length and coder into current length and coder. - */ - long stringConcatMix(long lengthCoder, char value); - /** * Creates helper for string concatenation. *

    diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java index dda9accd8b9..b30592a4ebd 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java @@ -30,6 +30,7 @@ import java.lang.classfile.constantpool.ClassEntry; import java.lang.classfile.constantpool.ConstantPool; import java.lang.classfile.constantpool.ConstantPoolBuilder; import java.lang.classfile.constantpool.PoolEntry; +import java.lang.runtime.ExactConversionsSupport; import java.util.Arrays; import jdk.internal.access.JavaLangAccess; @@ -275,8 +276,11 @@ public final class BufWriterImpl implements BufWriter { void writeUtfEntry(String str) { int strlen = str.length(); int countNonZeroAscii = JLA.countNonZeroAscii(str); - int utflen = utfLen(str, countNonZeroAscii); - Util.checkU2(utflen, "utf8 length"); + long utflenLong = utfLen(str, countNonZeroAscii); + if (!ExactConversionsSupport.isLongToCharExact(utflenLong)) { + throw new IllegalArgumentException("utf8 length out of range of u2: " + utflenLong); + } + int utflen = (int)utflenLong; reserveSpace(utflen + 3); int offset = this.offset; diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java index 6dbde898ee5..921255d5398 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -309,7 +309,7 @@ public record ClassRemapperImpl(Function mapFunction) impl case Signature.ClassTypeSig cts -> Signature.ClassTypeSig.of( cts.outerType().map(this::mapSignature).orElse(null), - map(cts.classDesc()), + Util.toInternalName(map(cts.classDesc())), cts.typeArgs().stream().map(ta -> switch (ta) { case Signature.TypeArg.Unbounded u -> u; case Signature.TypeArg.Bounded bta -> Signature.TypeArg.bounded( diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java index 5486b58f04e..06c5abb95a8 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -73,7 +73,7 @@ public final class SignaturesImpl { while (!match(')')) { if (paramTypes == null) paramTypes = new ArrayList<>(); - paramTypes.add(typeSig()); + paramTypes.add(validateNonVoid(typeSig())); } Signature returnType = typeSig(); ArrayList throwsTypes = null; @@ -113,8 +113,22 @@ public final class SignaturesImpl { RefTypeSig classBound = null; ArrayList interfaceBounds = null; require(':'); - if (sig.charAt(sigp) != ':') - classBound = referenceTypeSig(); + if (sig.charAt(sigp) != ':') { + int p = nextIdentifierEnd(sig, sigp); + // For non-identifier chars: + // . / < indicates class type (inner, package, type arg) + // [ indicates array type + // ; indicates class/type var type + // > and : are illegal, such as in + if (p < sig.length()) { + char limit = sig.charAt(p); + if (limit != '>' && limit != ':') { + classBound = referenceTypeSig(); + } + } + // If classBound is absent here, we start tokenizing + // next type parameter, which can trigger failures + } while (match(':')) { if (interfaceBounds == null) interfaceBounds = new ArrayList<>(); @@ -226,13 +240,9 @@ public final class SignaturesImpl { */ private int requireIdentifier() { int start = sigp; - l: while (sigp < sig.length()) { - switch (sig.charAt(sigp)) { - case '.', ';', '[', '/', '<', '>', ':' -> { - break l; - } - } + if (isNonIdentifierChar(sig.charAt(sigp))) + break; sigp++; } if (start == sigp) { @@ -241,6 +251,77 @@ public final class SignaturesImpl { return sigp; } + // Non-identifier chars in ascii 0 to 63, note [ is larger + private static final long SMALL_NON_IDENTIFIER_CHARS_SET = (1L << '.') + | (1L << ';') + | (1L << '/') + | (1L << '<') + | (1L << '>') + | (1L << ':'); + + private static boolean isNonIdentifierChar(char c) { + return c < Long.SIZE ? (SMALL_NON_IDENTIFIER_CHARS_SET & (1L << c)) != 0 : c == '['; + } + + /// {@return exclusive end of the next identifier} + public static int nextIdentifierEnd(String st, int start) { + int end = st.length(); + for (int i = start; i < end; i++) { + if (isNonIdentifierChar(st.charAt(i))) { + return i; + } + } + return end; + } + + /// Validates this string as a simple identifier. + public static String validateIdentifier(String st) { + var len = st.length(); // implicit null check + if (len == 0 || nextIdentifierEnd(st, 0) != len) { + throw new IllegalArgumentException("Not a valid identifier: " + st); + } + return st; + } + + /// Validates this string as slash-separated one or more identifiers. + public static String validatePackageSpecifierPlusIdentifier(String st) { + int nextIdentifierStart = 0; + int len = st.length(); + while (nextIdentifierStart < len) { + int end = nextIdentifierEnd(st, nextIdentifierStart); + if (end == len) + return st; + if (end == nextIdentifierStart || st.charAt(end) != '/') + throw new IllegalArgumentException("Not a class name: " + st); + nextIdentifierStart = end + 1; + } + // Couldn't get an identifier initially or after a separator. + throw new IllegalArgumentException("Not a class name: " + st); + } + + /// Validates the signature to be non-void (a valid field type). + public static Signature validateNonVoid(Signature incoming) { + Objects.requireNonNull(incoming); + if (incoming instanceof Signature.BaseTypeSig baseType && baseType.baseType() == 'V') + throw new IllegalArgumentException("void"); + return incoming; + } + + /// Returns the validated immutable argument list or fails with IAE. + public static List validateArgumentList(Signature[] signatures) { + return validateArgumentList(List.of(signatures)); + } + + /// Returns the validated immutable argument list or fails with IAE. + public static List validateArgumentList(List signatures) { + var res = List.copyOf(signatures); // deep null checks + for (var sig : signatures) { + if (sig instanceof Signature.BaseTypeSig baseType && baseType.baseType() == 'V') + throw new IllegalArgumentException("void"); + } + return res; + } + public static record BaseTypeSigImpl(char baseType) implements Signature.BaseTypeSig { @Override @@ -316,13 +397,13 @@ public final class SignaturesImpl { private static StringBuilder printTypeParameters(List typeParameters) { var sb = new StringBuilder(); - if (typeParameters != null && !typeParameters.isEmpty()) { + if (!typeParameters.isEmpty()) { sb.append('<'); for (var tp : typeParameters) { sb.append(tp.identifier()).append(':'); if (tp.classBound().isPresent()) sb.append(tp.classBound().get().signatureString()); - if (tp.interfaceBounds() != null) for (var is : tp.interfaceBounds()) + for (var is : tp.interfaceBounds()) sb.append(':').append(is.signatureString()); } sb.append('>'); @@ -337,7 +418,7 @@ public final class SignaturesImpl { public String signatureString() { var sb = printTypeParameters(typeParameters); sb.append(superclassSignature.signatureString()); - if (superinterfaceSignatures != null) for (var in : superinterfaceSignatures) + for (var in : superinterfaceSignatures) sb.append(in.signatureString()); return sb.toString(); } diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java b/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java index 7e6384dd1a4..6411c939549 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java @@ -230,7 +230,7 @@ public final class Util { public static IllegalArgumentException outOfRangeException(int value, String fieldName, String typeName) { return new IllegalArgumentException( - String.format("%s out of range of %d: %d", fieldName, typeName, value)); + String.format("%s out of range of %s: %d", fieldName, typeName, value)); } /// Ensures the given mask won't be truncated when written as an access flag diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java index bb862578b34..07406b2ee7f 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java @@ -1513,13 +1513,6 @@ public final class VerifierImpl { } } - // Return TRUE if all code paths starting with start_bc_offset end in - // bytecode athrow or loop. - boolean ends_in_athrow(int start_bc_offset) { - log_info("unimplemented VerifierImpl.ends_in_athrow"); - return true; - } - boolean verify_invoke_init(RawBytecodeHelper bcs, int ref_class_index, VerificationType ref_class_type, VerificationFrame current_frame, int code_length, boolean in_try_block, boolean this_uninit, ConstantPoolWrapper cp, VerificationTable stackmap_table) { @@ -1532,16 +1525,6 @@ public final class VerifierImpl { verifyError("Bad method call"); } if (in_try_block) { - for(var exhandler : _method.exceptionTable()) { - int start_pc = exhandler[0]; - int end_pc = exhandler[1]; - - if (bci >= start_pc && bci < end_pc) { - if (!ends_in_athrow(exhandler[2])) { - verifyError("Bad method call from after the start of a try block"); - } - } - } verify_exception_handler_targets(bci, true, current_frame, stackmap_table); } current_frame.initialize_object(type, current_type()); diff --git a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java index c9c6b53fcda..820dfe90073 100644 --- a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java +++ b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java @@ -174,7 +174,9 @@ public final class JdkConsoleImpl implements JdkConsole { ioe.addSuppressed(x); } if (ioe != null) { - Arrays.fill(passwd, ' '); + if (passwd != null) { + Arrays.fill(passwd, ' '); + } try { if (reader instanceof LineReader lr) { lr.zeroOut(); diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java index 79e718c76e5..e062e1629ff 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -137,6 +137,45 @@ public final class ImageReader implements AutoCloseable { return reader.findNode(name); } + /** + * Returns a resource node in the given module, or null if no resource of + * that name exists. + * + *

    This is equivalent to: + *

    {@code
    +     * findNode("/modules/" + moduleName + "/" + resourcePath)
    +     * }
    + * but more performant, and returns {@code null} for directories. + * + * @param moduleName The module name of the requested resource. + * @param resourcePath Trailing module-relative resource path, not starting + * with {@code '/'}. + */ + public Node findResourceNode(String moduleName, String resourcePath) + throws IOException { + ensureOpen(); + return reader.findResourceNode(moduleName, resourcePath); + } + + /** + * Returns whether a resource exists in the given module. + * + *

    This is equivalent to: + *

    {@code
    +     * findResourceNode(moduleName, resourcePath) != null
    +     * }
    + * but more performant, and will not create or cache new nodes. + * + * @param moduleName The module name of the resource being tested for. + * @param resourcePath Trailing module-relative resource path, not starting + * with {@code '/'}. + */ + public boolean containsResource(String moduleName, String resourcePath) + throws IOException { + ensureOpen(); + return reader.containsResource(moduleName, resourcePath); + } + /** * Returns a copy of the content of a resource node. The buffer returned by * this method is not cached by the node, and each call returns a new array @@ -276,10 +315,7 @@ public final class ImageReader implements AutoCloseable { * Returns a node with the given name, or null if no resource or directory of * that name exists. * - *

    This is the only public API by which anything outside this class can access - * {@code Node} instances either directly, or by resolving symbolic links. - * - *

    Note also that there is no reentrant calling back to this method from within + *

    Note that there is no reentrant calling back to this method from within * the node handling code. * * @param name an absolute, {@code /}-separated path string, prefixed with either @@ -291,6 +327,9 @@ public final class ImageReader implements AutoCloseable { // We cannot get the root paths ("/modules" or "/packages") here // because those nodes are already in the nodes cache. if (name.startsWith(MODULES_ROOT + "/")) { + // This may perform two lookups, one for a directory (in + // "/modules/...") and one for a non-prefixed resource + // (with "/modules" removed). node = buildModulesNode(name); } else if (name.startsWith(PACKAGES_ROOT + "/")) { node = buildPackagesNode(name); @@ -307,6 +346,55 @@ public final class ImageReader implements AutoCloseable { return node; } + /** + * Returns a resource node in the given module, or null if no resource of + * that name exists. + * + *

    Note that there is no reentrant calling back to this method from within + * the node handling code. + */ + Node findResourceNode(String moduleName, String resourcePath) { + // Unlike findNode(), this method makes only one lookup in the + // underlying jimage, but can only reliably return resource nodes. + if (moduleName.indexOf('/') >= 0) { + throw new IllegalArgumentException("invalid module name: " + moduleName); + } + String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath; + // Synchronize as tightly as possible to reduce locking contention. + synchronized (this) { + Node node = nodes.get(nodeName); + if (node == null) { + ImageLocation loc = findLocation(moduleName, resourcePath); + if (loc != null && isResource(loc)) { + node = newResource(nodeName, loc); + nodes.put(node.getName(), node); + } + return node; + } else { + return node.isResource() ? node : null; + } + } + } + + /** + * Returns whether a resource exists in the given module. + * + *

    This method is expected to be called frequently for resources + * which do not exist in the given module (e.g. as part of classpath + * search). As such, it skips checking the nodes cache and only checks + * for an entry in the jimage file, as this is faster if the resource + * is not present. This also means it doesn't need synchronization. + */ + boolean containsResource(String moduleName, String resourcePath) { + if (moduleName.indexOf('/') >= 0) { + throw new IllegalArgumentException("invalid module name: " + moduleName); + } + // If the given module name is 'modules', then 'isResource()' + // returns false to prevent false positives. + ImageLocation loc = findLocation(moduleName, resourcePath); + return loc != null && isResource(loc); + } + /** * Builds a node in the "/modules/..." namespace. * diff --git a/src/java.base/share/classes/jdk/internal/loader/BootLoader.java b/src/java.base/share/classes/jdk/internal/loader/BootLoader.java index bc5bd9d4265..72c7e7e7451 100644 --- a/src/java.base/share/classes/jdk/internal/loader/BootLoader.java +++ b/src/java.base/share/classes/jdk/internal/loader/BootLoader.java @@ -75,9 +75,13 @@ public class BootLoader { private static final ConcurrentHashMap CLASS_LOADER_VALUE_MAP = new ConcurrentHashMap<>(); - // native libraries loaded by the boot class loader - private static final NativeLibraries NATIVE_LIBS - = NativeLibraries.newInstance(null); + // Holder has the field(s) that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + private static class Holder { + // native libraries loaded by the boot class loader + private static final NativeLibraries NATIVE_LIBS + = NativeLibraries.newInstance(null); + } /** * Returns the unnamed module for the boot loader. @@ -104,7 +108,7 @@ public class BootLoader { * Returns NativeLibraries for the boot class loader. */ public static NativeLibraries getNativeLibraries() { - return NATIVE_LIBS; + return Holder.NATIVE_LIBS; } /** diff --git a/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java b/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java index 44eaab0e83a..98cedb0b3bf 100644 --- a/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java +++ b/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -153,7 +153,7 @@ public final class NativeLibraries { } // cannot be loaded by other class loaders - if (loadedLibraryNames.contains(name)) { + if (Holder.loadedLibraryNames.contains(name)) { throw new UnsatisfiedLinkError("Native Library " + name + " already loaded in another classloader"); } @@ -203,7 +203,7 @@ public final class NativeLibraries { NativeLibraryContext.pop(); } // register the loaded native library - loadedLibraryNames.add(name); + Holder.loadedLibraryNames.add(name); libraries.put(name, lib); return lib; } finally { @@ -243,6 +243,11 @@ public final class NativeLibraries { return lib; } + // Called at the end of AOTCache assembly phase. + public void clear() { + libraries.clear(); + } + private NativeLibrary findFromPaths(String[] paths, Class fromClass, String name) { for (String path : paths) { File libfile = new File(path, System.mapLibraryName(name)); @@ -368,7 +373,7 @@ public final class NativeLibraries { acquireNativeLibraryLock(name); try { /* remove the native library name */ - if (!loadedLibraryNames.remove(name)) { + if (!Holder.loadedLibraryNames.remove(name)) { throw new IllegalStateException(name + " has already been unloaded"); } NativeLibraryContext.push(UNLOADER); @@ -395,9 +400,13 @@ public final class NativeLibraries { static final String[] USER_PATHS = ClassLoaderHelper.parsePath(StaticProperty.javaLibraryPath()); } - // All native libraries we've loaded. - private static final Set loadedLibraryNames = + // Holder has the fields that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + static class Holder { + // All native libraries we've loaded. + private static final Set loadedLibraryNames = ConcurrentHashMap.newKeySet(); + } // reentrant lock class that allows exact counting (with external synchronization) @SuppressWarnings("serial") diff --git a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java index 32399310bd3..e90ca4d75f1 100644 --- a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java +++ b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java @@ -27,8 +27,6 @@ package jdk.internal.math; import jdk.internal.vm.annotation.Stable; -import java.util.Arrays; - /** * A class for converting between ASCII and decimal representations of a single * or double precision floating point number. Most conversions are provided via @@ -102,7 +100,6 @@ public class FloatingDecimal{ * values into an ASCII String representation. */ public interface BinaryToASCIIConverter { - int getChars(byte[] result); /** * Retrieves the decimal exponent most closely corresponding to this value. @@ -115,21 +112,7 @@ public class FloatingDecimal{ * @param digits The digit array. * @return The number of valid digits copied into the array. */ - int getDigits(char[] digits); - - /** - * Indicates the sign of the value. - * @return {@code value < 0.0}. - */ - boolean isNegative(); - - /** - * Indicates whether the value is either infinite or not a number. - * - * @return true if and only if the value is NaN - * or infinite. - */ - boolean isExceptional(); + int getDigits(byte[] digits); /** * Indicates whether the value was rounded up during the binary to ASCII @@ -147,63 +130,9 @@ public class FloatingDecimal{ boolean decimalDigitsExact(); } - /** - * A BinaryToASCIIConverter which represents NaN - * and infinite values. - */ - private static class ExceptionalBinaryToASCIIBuffer implements BinaryToASCIIConverter { - private final String image; - private final boolean isNegative; - - public ExceptionalBinaryToASCIIBuffer(String image, boolean isNegative) { - this.image = image; - this.isNegative = isNegative; - } - - @Override - @SuppressWarnings("deprecation") - public int getChars(byte[] chars) { - image.getBytes(0, image.length(), chars, 0); - return image.length(); - } - - @Override - public int getDecimalExponent() { - throw new IllegalArgumentException("Exceptional value does not have an exponent"); - } - - @Override - public int getDigits(char[] digits) { - throw new IllegalArgumentException("Exceptional value does not have digits"); - } - - @Override - public boolean isNegative() { - return isNegative; - } - - @Override - public boolean isExceptional() { - return true; - } - - @Override - public boolean digitsRoundedUp() { - throw new IllegalArgumentException("Exceptional value is not rounded"); - } - - @Override - public boolean decimalDigitsExact() { - throw new IllegalArgumentException("Exceptional value is not exact"); - } - } - private static final String INFINITY_REP = "Infinity"; private static final String NAN_REP = "NaN"; - private static final BinaryToASCIIConverter B2AC_POSITIVE_INFINITY = new ExceptionalBinaryToASCIIBuffer(INFINITY_REP, false); - private static final BinaryToASCIIConverter B2AC_NEGATIVE_INFINITY = new ExceptionalBinaryToASCIIBuffer("-" + INFINITY_REP, true); - private static final BinaryToASCIIConverter B2AC_NOT_A_NUMBER = new ExceptionalBinaryToASCIIBuffer(NAN_REP, false); private static final BinaryToASCIIConverter B2AC_POSITIVE_ZERO = new BinaryToASCIIBuffer(false, new byte[]{'0'}); private static final BinaryToASCIIConverter B2AC_NEGATIVE_ZERO = new BinaryToASCIIBuffer(true, new byte[]{'0'}); @@ -255,21 +184,11 @@ public class FloatingDecimal{ } @Override - public int getDigits(char[] digits) { + public int getDigits(byte[] digits) { System.arraycopy(this.digits, firstDigitIndex, digits, 0, this.nDigits); return this.nDigits; } - @Override - public boolean isNegative() { - return isNegative; - } - - @Override - public boolean isExceptional() { - return false; - } - @Override public boolean digitsRoundedUp() { return decimalDigitsRoundedUp; @@ -826,83 +745,6 @@ public class FloatingDecimal{ 61, }; - /** - * Converts the decimal representation of a floating-point number into its - * ASCII character representation and stores it in the provided byte array. - * - * @param result the byte array to store the ASCII representation, must have length at least 26 - * @return the number of characters written to the result array - */ - public int getChars(byte[] result) { - assert nDigits <= 19 : nDigits; // generous bound on size of nDigits - int i = 0; - if (isNegative) { - result[0] = '-'; - i = 1; - } - if (decExponent > 0 && decExponent < 8) { - // print digits.digits. - int charLength = Math.min(nDigits, decExponent); - System.arraycopy(digits, firstDigitIndex, result, i, charLength); - i += charLength; - if (charLength < decExponent) { - charLength = decExponent - charLength; - Arrays.fill(result, i, i + charLength, (byte) '0'); - i += charLength; - result[i++] = '.'; - result[i++] = '0'; - } else { - result[i++] = '.'; - if (charLength < nDigits) { - int t = nDigits - charLength; - System.arraycopy(digits, firstDigitIndex + charLength, result, i, t); - i += t; - } else { - result[i++] = '0'; - } - } - } else if (decExponent <= 0 && decExponent > -3) { - result[i++] = '0'; - result[i++] = '.'; - if (decExponent != 0) { - Arrays.fill(result, i, i-decExponent, (byte) '0'); - i -= decExponent; - } - System.arraycopy(digits, firstDigitIndex, result, i, nDigits); - i += nDigits; - } else { - result[i++] = digits[firstDigitIndex]; - result[i++] = '.'; - if (nDigits > 1) { - System.arraycopy(digits, firstDigitIndex+1, result, i, nDigits - 1); - i += nDigits - 1; - } else { - result[i++] = '0'; - } - result[i++] = 'E'; - int e; - if (decExponent <= 0) { - result[i++] = '-'; - e = -decExponent + 1; - } else { - e = decExponent - 1; - } - // decExponent has 1, 2, or 3, digits - if (e <= 9) { - result[i++] = (byte) (e + '0'); - } else if (e <= 99) { - result[i++] = (byte) (e / 10 + '0'); - result[i++] = (byte) (e % 10 + '0'); - } else { - result[i++] = (byte) (e / 100 + '0'); - e %= 100; - result[i++] = (byte) (e / 10 + '0'); - result[i++] = (byte) (e % 10 + '0'); - } - } - return i; - } - } private static final ThreadLocal threadLocalBinaryToASCIIBuffer = @@ -1707,9 +1549,9 @@ public class FloatingDecimal{ // Discover obvious special cases of NaN and Infinity. if ( binExp == (int)(DoubleConsts.EXP_BIT_MASK>>EXP_SHIFT) ) { if ( fractBits == 0L ){ - return isNegative ? B2AC_NEGATIVE_INFINITY : B2AC_POSITIVE_INFINITY; + throw new IllegalArgumentException((isNegative ? "-" : "") + INFINITY_REP); } else { - return B2AC_NOT_A_NUMBER; + throw new IllegalArgumentException(NAN_REP); } } // Finish unpacking diff --git a/src/java.base/share/classes/jdk/internal/math/MathUtils.java b/src/java.base/share/classes/jdk/internal/math/MathUtils.java index 0de07f8ed60..172090facc0 100644 --- a/src/java.base/share/classes/jdk/internal/math/MathUtils.java +++ b/src/java.base/share/classes/jdk/internal/math/MathUtils.java @@ -26,12 +26,14 @@ package jdk.internal.math; import jdk.internal.vm.annotation.Stable; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** * This class exposes package private utilities for other classes. * Thus, all methods are assumed to be invoked with correct arguments, * so these are not checked at all. */ +@AOTSafeClassInitializer final class MathUtils { /* * For full details about this code see the following reference: diff --git a/src/java.base/share/classes/jdk/internal/misc/CDS.java b/src/java.base/share/classes/jdk/internal/misc/CDS.java index 72b8479de9a..b61743c1fb3 100644 --- a/src/java.base/share/classes/jdk/internal/misc/CDS.java +++ b/src/java.base/share/classes/jdk/internal/misc/CDS.java @@ -47,11 +47,13 @@ import jdk.internal.util.StaticProperty; public class CDS { // Must be in sync with cdsConfig.hpp - private static final int IS_DUMPING_ARCHIVE = 1 << 0; - private static final int IS_DUMPING_METHOD_HANDLES = 1 << 1; - private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 2; - private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 3; - private static final int IS_USING_ARCHIVE = 1 << 4; + private static final int IS_DUMPING_AOT_LINKED_CLASSES = 1 << 0; + private static final int IS_DUMPING_ARCHIVE = 1 << 1; + private static final int IS_DUMPING_METHOD_HANDLES = 1 << 2; + private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 3; + private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 4; + private static final int IS_USING_ARCHIVE = 1 << 5; + private static final int configStatus = getCDSConfigStatus(); /** @@ -82,6 +84,10 @@ public class CDS { return (configStatus & IS_DUMPING_STATIC_ARCHIVE) != 0; } + public static boolean isDumpingAOTLinkedClasses() { + return (configStatus & IS_DUMPING_AOT_LINKED_CLASSES) != 0; + } + public static boolean isSingleThreadVM() { return isDumpingStaticArchive(); } diff --git a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java index 39f433d4041..370c151af84 100644 --- a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java +++ b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java @@ -414,26 +414,18 @@ public final class SystemModuleFinders { * Returns {@code true} if the given resource exists, {@code false} * if not found. */ - private boolean containsResource(String resourcePath) throws IOException { - Objects.requireNonNull(resourcePath); + private boolean containsResource(String module, String name) throws IOException { + Objects.requireNonNull(name); if (closed) throw new IOException("ModuleReader is closed"); ImageReader imageReader = SystemImage.reader(); - if (imageReader != null) { - ImageReader.Node node = imageReader.findNode("/modules" + resourcePath); - return node != null && node.isResource(); - } else { - // not an images build - return false; - } + return imageReader != null && imageReader.containsResource(module, name); } @Override public Optional find(String name) throws IOException { - Objects.requireNonNull(name); - String resourcePath = "/" + module + "/" + name; - if (containsResource(resourcePath)) { - URI u = JNUA.create("jrt", resourcePath); + if (containsResource(module, name)) { + URI u = JNUA.create("jrt", "/" + module + "/" + name); return Optional.of(u); } else { return Optional.empty(); @@ -465,9 +457,7 @@ public final class SystemModuleFinders { if (closed) { throw new IOException("ModuleReader is closed"); } - String nodeName = "/modules/" + module + "/" + name; - ImageReader.Node node = reader.findNode(nodeName); - return (node != null && node.isResource()) ? node : null; + return reader.findResourceNode(module, name); } @Override diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java new file mode 100644 index 00000000000..89d15eb3439 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + + +import java.util.Objects; + +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; + +/** + * Thrown when an operation on {@link QuicTLSEngine} doesn't have the necessary + * QUIC keys for encrypting or decrypting packets. This can either be because + * the keys aren't available for a particular {@linkplain KeySpace keyspace} or + * the keys for the {@code keyspace} have been discarded. + */ +public final class QuicKeyUnavailableException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 8553365136999153478L; + + public QuicKeyUnavailableException(final String message, final KeySpace keySpace) { + super(Objects.requireNonNull(keySpace) + " keyspace: " + message); + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java new file mode 100644 index 00000000000..fd0b405069c --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +/** + * Supplies contextual 1-RTT information that's available in the QUIC implementation of the + * {@code java.net.http} module, to the QUIC TLS layer in the {@code java.base} module. + */ +public interface QuicOneRttContext { + + /** + * {@return the largest packet number that was acknowledged by + * the peer in the 1-RTT packet space} + */ + long getLargestPeerAckedPN(); +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java new file mode 100644 index 00000000000..5cf0c999fb9 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Objects; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLContextSpi; +import javax.net.ssl.SSLParameters; + +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.SSLContextImpl; + +/** + * Instances of this class act as a factory for creation + * of {@link QuicTLSEngine QUIC TLS engine}. + */ +public final class QuicTLSContext { + + // In this implementation, we have a dependency on + // sun.security.ssl.SSLContextImpl. We can only support + // Quic on SSLContext instances created by the default + // SunJSSE Provider + private final SSLContextImpl sslCtxImpl; + + /** + * {@return {@code true} if the given {@code sslContext} supports QUIC TLS, {@code false} otherwise} + * @param sslContext an {@link SSLContext} + */ + public static boolean isQuicCompatible(final SSLContext sslContext) { + boolean parametersSupported = isQuicCompatible(sslContext.getSupportedSSLParameters()); + if (!parametersSupported) { + return false; + } + // horrible hack - what we do here is try and get hold of a SSLContext + // that has already been initialised and configured with the HttpClient. + // We see if that SSLContext is created using an implementation of + // sun.security.ssl.SSLContextImpl. Since there's no API + // available to get hold of that underlying implementation, we use + // MethodHandle lookup to get access to the field which holds that + // detail. + final Object underlyingImpl = CONTEXT_SPI.get(sslContext); + if (!(underlyingImpl instanceof SSLContextImpl ssci)) { + return false; + } + return ssci.isUsableWithQuic(); + } + + /** + * {@return {@code true} if protocols of the given {@code parameters} support QUIC TLS, {@code false} otherwise} + */ + public static boolean isQuicCompatible(SSLParameters parameters) { + String[] protocols = parameters.getProtocols(); + return protocols != null && Arrays.asList(protocols).contains("TLSv1.3"); + } + + private static SSLContextImpl getSSLContextImpl( + final SSLContext sslContext) { + final Object underlyingImpl = CONTEXT_SPI.get(sslContext); + assert underlyingImpl instanceof SSLContextImpl; + return (SSLContextImpl) underlyingImpl; + } + + /** + * Constructs a QuicTLSContext for the given {@code sslContext} + * + * @param sslContext The SSLContext + * @throws IllegalArgumentException If the passed {@code sslContext} isn't + * supported by the QuicTLSContext + * @see #isQuicCompatible(SSLContext) + */ + public QuicTLSContext(final SSLContext sslContext) { + Objects.requireNonNull(sslContext); + if (!isQuicCompatible(sslContext)) { + throw new IllegalArgumentException( + "Cannot construct a QUIC TLS context with the given SSLContext"); + } + this.sslCtxImpl = getSSLContextImpl(sslContext); + } + + /** + * Creates a {@link QuicTLSEngine} using this context + *

    + * This method does not provide hints for session caching. + * + * @return the newly created QuicTLSEngine + */ + public QuicTLSEngine createEngine() { + return createEngine(null, -1); + } + + /** + * Creates a {@link QuicTLSEngine} using this context using + * advisory peer information. + *

    + * The provided parameters will be used as hints for session caching. + * The {@code peerHost} parameter will be used in the server_name extension, + * unless overridden later. + * + * @param peerHost The peer hostname or IP address. Can be null. + * @param peerPort The peer port, can be -1 if the port is unknown + * @return the newly created QuicTLSEngine + */ + public QuicTLSEngine createEngine(final String peerHost, final int peerPort) { + return new QuicTLSEngineImpl(this.sslCtxImpl, peerHost, peerPort); + } + + // This VarHandle is used to access the SSLContext::contextSpi + // field which is not publicly accessible. + // In this implementation, Quic is only supported for SSLContext + // instances whose underlying implementation is provided by a + // sun.security.ssl.SSLContextImpl + private static final VarHandle CONTEXT_SPI; + static { + try { + final MethodHandles.Lookup lookup = + MethodHandles.privateLookupIn(SSLContext.class, + MethodHandles.lookup()); + final VarHandle vh = lookup.findVarHandle(SSLContext.class, + "contextSpi", SSLContextSpi.class); + CONTEXT_SPI = vh; + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } +} + diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java new file mode 100644 index 00000000000..70ed86bbf01 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import javax.crypto.AEADBadTagException; +import javax.crypto.ShortBufferException; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.function.IntFunction; + +/** + * One instance of these per QUIC connection. Configuration methods not shown + * but would be similar to SSLEngine. + */ +public interface QuicTLSEngine { + + /** + * Represents the encryption level associated with a packet encryption or + * decryption. A QUIC connection has a current keyspace for sending and + * receiving which can be queried. + */ + enum KeySpace { + INITIAL, + HANDSHAKE, + RETRY, // Special algorithm used for this packet + ZERO_RTT, + ONE_RTT + } + + enum HandshakeState { + /** + * Need to receive a CRYPTO frame + */ + NEED_RECV_CRYPTO, + /** + * Need to receive a HANDSHAKE_DONE frame from server to complete the + * handshake, but application data can be sent in this state (client + * only state). + */ + NEED_RECV_HANDSHAKE_DONE, + /** + * Need to send a CRYPTO frame + */ + NEED_SEND_CRYPTO, + /** + * Need to send a HANDSHAKE_DONE frame to complete the handshake, but + * application data can be sent in this state (server only state) + */ + NEED_SEND_HANDSHAKE_DONE, + /** + * Need to execute a task + */ + NEED_TASK, + /** + * Handshake is confirmed, as specified in section 4.1.2 of RFC-9001 + */ + // On client side this happens when client receives HANDSHAKE_DONE + // frame. On server side this happens when the TLS stack has both + // sent a Finished message and verified the peer's Finished message. + HANDSHAKE_CONFIRMED, + } + + /** + * {@return the QUIC versions supported by this engine} + */ + Set getSupportedQuicVersions(); + + /** + * If {@code mode} is {@code true} then configures this QuicTLSEngine to + * operate in client mode. If {@code false}, then this QuicTLSEngine + * operates in server mode. + * + * @param mode true to make this QuicTLSEngine operate in client + * mode, false otherwise + */ + void setUseClientMode(boolean mode); + + /** + * {@return true if this QuicTLSEngine is operating in client mode, false + * otherwise} + */ + boolean getUseClientMode(); + + /** + * {@return the SSLParameters in effect for this engine.} + */ + SSLParameters getSSLParameters(); + + /** + * Sets the {@code SSLParameters} to be used by this engine + * + * @param sslParameters the SSLParameters + * @throws IllegalArgumentException if + * {@linkplain SSLParameters#getProtocols() TLS protocol versions} on the + * {@code sslParameters} is either empty or contains anything other + * than {@code TLSv1.3} + * @throws NullPointerException if {@code sslParameters} is null + */ + void setSSLParameters(SSLParameters sslParameters); + + /** + * {@return the most recent application protocol value negotiated by the + * engine. Returns null if no application protocol has yet been negotiated + * by the engine} + */ + String getApplicationProtocol(); + + /** + * {@return the SSLSession} + * + * @see SSLEngine#getSession() + */ + SSLSession getSession(); + + /** + * Returns the SSLSession being constructed during a QUIC handshake. + * + * @return null if this instance is not currently handshaking, or if the + * current handshake has not progressed far enough to create + * a basic SSLSession. Otherwise, this method returns the + * {@code SSLSession} currently being negotiated. + * + * @see SSLEngine#getHandshakeSession() + */ + SSLSession getHandshakeSession(); + + /** + * Returns the current handshake state of the connection. Sometimes packets + * that could be decrypted can be received before the handshake has + * completed, but should not be decrypted until it is complete + * + * @return the HandshakeState + */ + HandshakeState getHandshakeState(); + + /** + * Returns true if the TLS handshake is considered complete. + *

    + * The TLS handshake is considered complete when the TLS stack + * has reported that the handshake is complete. This happens when + * the TLS stack has both sent a {@code Finished} message and verified + * the peer's {@code Finished} message. + * + * @return true if TLS handshake is complete, false otherwise. + */ + boolean isTLSHandshakeComplete(); + + /** + * {@return the current sending key space (encryption level)} + */ + KeySpace getCurrentSendKeySpace(); + + /** + * Checks whether the keys for the given key space are available. + *

    + * Keys are available when they are already computed and not discarded yet. + * + * @param keySpace key space to check + * @return true if the given keys are available + */ + boolean keysAvailable(KeySpace keySpace); + + /** + * Discard the keys used by the {@code keySpace}. + *

    + * Once the keys for a particular {@code keySpace} have been discarded, the + * keySpace will no longer be able to + * {@linkplain #encryptPacket(KeySpace, long, IntFunction, + * ByteBuffer, ByteBuffer) encrypt} or + * {@linkplain #decryptPacket(KeySpace, long, int, ByteBuffer, int, ByteBuffer) + * decrypt} packets. + * + * @param keySpace The keyspace whose current keys should be discarded + */ + void discardKeys(KeySpace keySpace); + + /** + * Provide quic_transport_parameters for inclusion in handshake message. + * + * @param params encoded quic_transport_parameters + */ + void setLocalQuicTransportParameters(ByteBuffer params); + + /** + * Reset the handshake state and produce a new ClientHello message. + * + * When a Quic client receives a Version Negotiation packet, + * it restarts the handshake by calling this method after updating the + * {@linkplain #setLocalQuicTransportParameters(ByteBuffer) transport parameters} + * with the new version information. + */ + void restartHandshake() throws IOException; + + /** + * Set consumer for quic_transport_parameters sent by the remote side. + * Consumer will receive a byte buffer containing the value of + * quic_transport_parameters extension sent by the remote endpoint. + * + * @param consumer consumer for remote quic transport parameters + */ + void setRemoteQuicTransportParametersConsumer( + QuicTransportParametersConsumer consumer); + + /** + * Derive initial keys for the given QUIC version and connection ID + * @param quicVersion QUIC protocol version + * @param connectionId initial destination connection ID + * @throws IllegalArgumentException if the {@code quicVersion} isn't + * {@linkplain #getSupportedQuicVersions() supported} on this + * {@code QuicTLSEngine} + */ + void deriveInitialKeys(QuicVersion quicVersion, ByteBuffer connectionId) throws IOException; + + /** + * Get the sample size for header protection algorithm + * + * @param keySpace Packet key space + * @return required sample size for header protection + * @throws IllegalArgumentException when keySpace does not require + * header protection + */ + int getHeaderProtectionSampleSize(KeySpace keySpace); + + /** + * Compute the header protection mask for the given sample, + * packet key space and direction (incoming/outgoing). + * + * @param keySpace Packet key space + * @param incoming true for incoming packets, false for outgoing + * @param sample sampled data + * @return mask bytes, at least 5. + * @throws IllegalArgumentException when keySpace does not require + * header protection or sample length is different from required + * @see #getHeaderProtectionSampleSize(KeySpace) + * @spec https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati + * RFC 9001, Section 5.4.1 Header Protection Application + */ + ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, + boolean incoming, ByteBuffer sample) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Get the authentication tag size. Encryption adds this number of bytes. + * + * @return authentication tag size + */ + int getAuthTagSize(); + + /** + * Encrypt into {@code output}, the given {@code packetPayload} bytes using the + * keys for the given {@code keySpace}. + *

    + * Before encrypting the {@code packetPayload}, this method invokes the {@code headerGenerator} + * passing it the key phase corresponding to the encryption key that's in use. + * For {@code KeySpace}s where key phase isn't applicable, the {@code headerGenerator} will + * be invoked with a value of {@code 0} for the key phase. + *

    + * The {@code headerGenerator} is expected to return a {@code ByteBuffer} representing the + * packet header and where applicable, the returned header must contain the key phase + * that was passed to the {@code headerGenerator}. The packet header will be used as + * the Additional Authentication Data (AAD) for encrypting the {@code packetPayload}. + *

    + * Upon return, the {@code output} will contain the encrypted packet payload bytes + * and the authentication tag. The {@code packetPayload} and the packet header, returned + * by the {@code headerGenerator}, will have their {@code position} equal to their + * {@code limit}. The limit of either of those buffers will not have changed. + *

    + * It is recommended to do the encryption in place by using slices of a bigger + * buffer as the input and output buffer: + *

    +     *          +--------+-------------------+
    +     * input:   | header | plaintext payload |
    +     *          +--------+-------------------+----------+
    +     * output:           | encrypted payload | AEAD tag |
    +     *                   +-------------------+----------+
    +     * 
    + * + * @param keySpace Packet key space + * @param packetNumber full packet number + * @param headerGenerator an {@link IntFunction} which takes a key phase and returns + * the packet header + * @param packetPayload buffer containing unencrypted packet payload + * @param output buffer into which the encrypted packet payload will be written + * @throws QuicKeyUnavailableException if keys are not available + * @throws QuicTransportException if encrypting the packet would result + * in exceeding the AEAD cipher confidentiality limit + */ + void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, + ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException; + + /** + * Decrypt the given packet bytes using keys for the given packet key space. + * Header protection must be removed before calling this method. + *

    + * The input buffer contains the packet header and the encrypted packet payload. + * The packet header (first {@code headerLength} bytes of the input buffer) + * is consumed by this method, but is not decrypted. + * The packet payload (bytes following the packet header) is decrypted + * by this method. This method consumes the entire input buffer. + *

    + * The decrypted payload bytes are written + * to the output buffer. + *

    + * It is recommended to do the decryption in place by using slices of a bigger + * buffer as the input and output buffer: + *

    +     *          +--------+-------------------+----------+
    +     * input:   | header | encrypted payload | AEAD tag |
    +     *          +--------+-------------------+----------+
    +     * output:           | decrypted payload |
    +     *                   +-------------------+
    +     * 
    + * + * @param keySpace Packet key space + * @param packetNumber full packet number + * @param keyPhase key phase bit (0 or 1) found on the packet, or -1 + * if the packet does not have a key phase bit + * @param packet buffer containing encrypted packet bytes + * @param headerLength length of the packet header + * @param output buffer where decrypted packet bytes will be stored + * @throws IllegalArgumentException if keyPhase bit is invalid + * @throws QuicKeyUnavailableException if keys are not available + * @throws AEADBadTagException if the provided packet's authentication tag + * is incorrect + * @throws QuicTransportException if decrypting the invalid packet resulted + * in exceeding the AEAD cipher integrity limit + */ + void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) + throws IllegalArgumentException, QuicKeyUnavailableException, + AEADBadTagException, QuicTransportException, ShortBufferException; + + /** + * Sign the provided retry packet. Input buffer contains the retry packet + * payload. Integrity tag is stored in the output buffer. + * + * @param version Quic version + * @param originalConnectionId original destination connection ID, + * without length + * @param packet retry packet bytes without tag + * @param output buffer where integrity tag will be stored + * @throws ShortBufferException if output buffer is too short to + * hold the tag + * @throws IllegalArgumentException if originalConnectionId is + * longer than 255 bytes + * @throws IllegalArgumentException if {@code version} isn't + * {@linkplain #getSupportedQuicVersions() supported} + */ + void signRetryPacket(QuicVersion version, ByteBuffer originalConnectionId, + ByteBuffer packet, ByteBuffer output) throws ShortBufferException, QuicTransportException; + + /** + * Verify the provided retry packet. + * + * @param version Quic version + * @param originalConnectionId original destination connection ID, + * without length + * @param packet retry packet bytes with tag + * @throws AEADBadTagException if integrity tag is invalid + * @throws IllegalArgumentException if originalConnectionId is + * longer than 255 bytes + * @throws IllegalArgumentException if {@code version} isn't + * {@linkplain #getSupportedQuicVersions() supported} + */ + void verifyRetryPacket(QuicVersion version, ByteBuffer originalConnectionId, + ByteBuffer packet) throws AEADBadTagException, QuicTransportException; + + /** + * If the current handshake state is {@link HandshakeState#NEED_SEND_CRYPTO} + * meaning that a CRYPTO frame needs to be sent then this method is called + * to obtain the contents of the frame. Current handshake state + * can be obtained from {@link #getHandshakeState()}, and the current + * key space can be obtained with {@link #getCurrentSendKeySpace()} + * The bytes returned by this call are used to build a CRYPTO frame. + * + * @param keySpace the key space of the packet in which the + * requested data will be placed + * @return buffer containing data that will be put by caller in a CRYPTO + * frame, or null if there are no more handshake bytes to send in + * this key space at this time. + */ + ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException; + + /** + * This method consumes crypto stream. + * + * @param keySpace the key space of the packet in which the provided + * crypto data was encountered. + * @param payload contents of the next CRYPTO frame + * @throws IllegalArgumentException if keySpace is ZERORTT or + * payload is empty + * @throws QuicTransportException if the handshake failed + */ + void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) + throws QuicTransportException; + + /** + * Returns a delegated {@code Runnable} task for + * this {@code QuicTLSEngine}. + *

    + * {@code QuicTLSEngine} operations may require the results of + * operations that block, or may take an extended period of time to + * complete. This method is used to obtain an outstanding {@link + * java.lang.Runnable} operation (task). Each task must be assigned + * a thread (possibly the current) to perform the {@link + * java.lang.Runnable#run() run} operation. Once the + * {@code run} method returns, the {@code Runnable} object + * is no longer needed and may be discarded. + *

    + * A call to this method will return each outstanding task + * exactly once. + *

    + * Multiple delegated tasks can be run in parallel. + * + * @return a delegated {@code Runnable} task, or null + * if none are available. + */ + Runnable getDelegatedTask(); + + /** + * Called to check if a {@code HANDSHAKE_DONE} frame needs to be sent by the + * server. This method will only be called for a {@code QuicTLSEngine} which + * is in {@linkplain #getUseClientMode() server mode}. If the current TLS handshake + * state is + * {@link HandshakeState#NEED_SEND_HANDSHAKE_DONE + * NEED_SEND_HANDSHAKE_DONE} then this method returns {@code true} and + * advances the TLS handshake state to + * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else + * returns {@code false}. + * + * @return true if handshake state was {@code NEED_SEND_HANDSHAKE_DONE}, + * false otherwise + * @throws IllegalStateException If this {@code QuicTLSEngine} is + * not in server mode + */ + boolean tryMarkHandshakeDone() throws IllegalStateException; + + /** + * Called when HANDSHAKE_DONE message is received from the server. This + * method will only be called for a {@code QuicTLSEngine} which is in + * {@linkplain #getUseClientMode() client mode}. If the current TLS handshake state + * is + * {@link HandshakeState#NEED_RECV_HANDSHAKE_DONE + * NEED_RECV_HANDSHAKE_DONE} then this method returns {@code true} and + * advances the TLS handshake state to + * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else + * returns {@code false}. + * + * @return true if handshake state was {@code NEED_RECV_HANDSHAKE_DONE}, + * false otherwise + * @throws IllegalStateException if this {@code QuicTLSEngine} is + * not in client mode + */ + boolean tryReceiveHandshakeDone() throws IllegalStateException; + + /** + * Called when the client and the server, during the connection creation + * handshake, have settled on a Quic version to use for the connection. This + * can happen either due to an explicit version negotiation (as outlined in + * Quic RFC) or the server accepting the Quic version that the client chose + * in its first INITIAL packet. In either of those cases, this method will + * be called. + * + * @param quicVersion the negotiated {@code QuicVersion} + * @throws IllegalArgumentException if the {@code quicVersion} isn't + * {@linkplain #getSupportedQuicVersions() supported} on this engine + */ + void versionNegotiated(QuicVersion quicVersion); + + /** + * Sets the {@link QuicOneRttContext} on the {@code QuicTLSEngine}. + *

    The {@code ctx} will be used by the {@code QuicTLSEngine} to access contextual 1-RTT + * data that might be required for the TLS operations. + * + * @param ctx the 1-RTT context to set + * @throws NullPointerException if {@code ctx} is null + */ + void setOneRttContext(QuicOneRttContext ctx); +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java new file mode 100644 index 00000000000..d081458d40e --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import sun.security.ssl.Alert; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * An enum to model Quic transport errors. + * Some errors have a single possible code value, some, like + * {@link #CRYPTO_ERROR} have a range of possible values. + * Usually, the value (a long) would be used instead of the + * enum, but the enum itself can be useful - for instance in + * switch statements. + * This enum models QUIC transport error codes as defined in + * RFC 9000, section 20.1. + */ +public enum QuicTransportErrors { + /** + * No error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint uses this with CONNECTION_CLOSE to signal that
    +     * the connection is being closed abruptly in the absence
    +     * of any error.
    +     * }
    + */ + NO_ERROR(0x00), + + /** + * Internal Error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * The endpoint encountered an internal error and cannot
    +     * continue with the connection.
    +     * }
    + */ + INTERNAL_ERROR(0x01), + + /** + * Connection refused error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * The server refused to accept a new connection.
    +     * }
    + */ + CONNECTION_REFUSED(0x02), + + /** + * Flow control error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint received more data than it permitted in its advertised data limits;
    +     * see Section 4.
    +     * }
    + * @see + * RFC 9000, Section 20.1: + *
    {@code
    +     * An endpoint received a frame for a stream identifier that exceeded its advertised
    +     * stream limit for the corresponding stream type.
    +     * }
    + */ + STREAM_LIMIT_ERROR(0x04), + + /** + * Stream state error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint received a frame for a stream that was not in a state that permitted
    +     * that frame; see Section 3.
    +     * }
    + * @see + * RFC 9000, Section 20.1: + *
    {@code
    +     * (1) An endpoint received a STREAM frame containing data that exceeded the previously
    +     *     established final size,
    +     * (2) an endpoint received a STREAM frame or a RESET_STREAM frame containing a final
    +     *     size that was lower than the size of stream data that was already received, or
    +     * (3) an endpoint received a STREAM frame or a RESET_STREAM frame containing a
    +     *     different final size to the one already established.
    +     * }
    + */ + FINAL_SIZE_ERROR(0x06), + + /** + * Frame encoding error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint received a frame that was badly formatted -- for instance,
    +     * a frame of an unknown type or an ACK frame that has more
    +     * acknowledgment ranges than the remainder of the packet could carry.
    +     * }
    + */ + FRAME_ENCODING_ERROR(0x07), + + /** + * Transport parameter error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint received transport parameters that were badly
    +     * formatted, included an invalid value, omitted a mandatory
    +     * transport parameter, included a forbidden transport
    +     * parameter, or were otherwise in error.
    +     * }
    + */ + TRANSPORT_PARAMETER_ERROR(0x08), + + /** + * Connection id limit error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * The number of connection IDs provided by the peer exceeds
    +     * the advertised active_connection_id_limit.
    +     * }
    + */ + CONNECTION_ID_LIMIT_ERROR(0x09), + + /** + * Protocol violiation error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint detected an error with protocol compliance that
    +     * was not covered by more specific error codes.
    +     * }
    + */ + PROTOCOL_VIOLATION(0x0a), + + /** + * Invalid token error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * A server received a client Initial that contained an invalid Token field.
    +     * }
    + */ + INVALID_TOKEN(0x0b), + + /** + * Application error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * The application or application protocol caused the connection to be closed.
    +     * }
    + */ + APPLICATION_ERROR(0x0c), + + /** + * Crypto buffer exceeded error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint has received more data in CRYPTO frames than it can buffer.
    +     * }
    + */ + CRYPTO_BUFFER_EXCEEDED(0x0d), + + /** + * Key update error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint detected errors in performing key updates; see Section 6 of [QUIC-TLS].
    +     * }
    + * @see Section 6 of RFC 9001 [QUIC-TLS] + */ + KEY_UPDATE_ERROR(0x0e), + + /** + * AEAD limit reached error + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint has reached the confidentiality or integrity limit
    +     * for the AEAD algorithm used by the given connection.
    +     * }
    + */ + AEAD_LIMIT_REACHED(0x0f), + + /** + * No viable path error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * An endpoint has determined that the network path is incapable of
    +     * supporting QUIC. An endpoint is unlikely to receive a
    +     * CONNECTION_CLOSE frame carrying this code except when the
    +     * path does not support a large enough MTU.
    +     * }
    + */ + NO_VIABLE_PATH(0x10), + + /** + * Error negotiating version. + * @spec https://www.rfc-editor.org/rfc/rfc9368#name-version-downgrade-preventio + * RFC 9368, Section 4 + */ + VERSION_NEGOTIATION_ERROR(0x11), + + /** + * Crypto error. + *

    + * From + * RFC 9000, Section 20.1: + *

    {@code
    +     * The cryptographic handshake failed. A range of 256 values is
    +     * reserved for carrying error codes specific to the cryptographic
    +     * handshake that is used. Codes for errors occurring when
    +     * TLS is used for the cryptographic handshake are described
    +     * in Section 4.8 of [QUIC-TLS].
    +     * }
    + * @see Section 4.8 of RFC 9001 [QUIC-TLS] + */ + CRYPTO_ERROR(0x0100, 0x01ff); + + private final long from; + private final long to; + + QuicTransportErrors(long code) { + this(code, code); + } + + QuicTransportErrors(long from, long to) { + assert from <= to; + this.from = from; + this.to = to; + } + + /** + * {@return the code for this transport error, if this error + * {@linkplain #hasCode() has a single possible code value}, + * {@code -1} otherwise} + */ + public long code() { return hasCode() ? from : -1;} + + /** + * {@return true if this error has a single possible code value} + */ + public boolean hasCode() { return from == to; } + + /** + * {@return true if this error has a range of possible code values} + */ + public boolean hasRange() { return from < to;} + + /** + * {@return the first possible code value in the range, or the + * code value if this error has a single possible code value} + */ + public long from() {return from;} + + /** + * {@return the last possible code value in the range, or the + * code value if this error has a single possible code value} + */ + public long to() { return to; } + + /** + * Tells whether the given {@code code} value corresponds to + * this error. + * @param code an error code value + * @return true if the given {@code code} value corresponds to + * this error. + */ + boolean isFor(long code) { + return code >= from && code <= to; + } + + /** + * {@return the {@link QuicTransportErrors} instance corresponding + * to the given {@code code} value, if any} + * @param code a {@code code} value + */ + public static Optional ofCode(long code) { + return Stream.of(values()).filter(e -> e.isFor(code)).findAny(); + } + + public static String toString(long code) { + Optional c = Stream.of(values()).filter(e -> e.isFor(code)).findAny(); + if (c.isEmpty()) return "Unknown [0x"+Long.toHexString(code) + "]"; + if (c.get().hasCode()) return c.get().toString(); + if (c.get() == CRYPTO_ERROR) + return c.get() + "|" + Alert.nameOf((byte)code); + return c.get() + " [0x" + Long.toHexString(code) + "]"; + + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java new file mode 100644 index 00000000000..3341cc527f2 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +/** + * Exception that wraps QUIC transport error codes. + * Thrown in response to packets or frames that violate QUIC protocol. + * This is a fatal exception; connection is always closed when this exception is caught. + * + *

    For a list of errors see: + * https://www.rfc-editor.org/rfc/rfc9000.html#name-transport-error-codes + */ +public final class QuicTransportException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 5259674758792412464L; + + private final QuicTLSEngine.KeySpace keySpace; + private final long frameType; + private final long errorCode; + + /** + * Constructs a new {@code QuicTransportException}. + * + * @param reason the reason why the exception occurred + * @param keySpace the key space in which the frame appeared. + * May be {@code null}, for instance, in + * case of {@link QuicTransportErrors#INTERNAL_ERROR}. + * @param frameType the frame type of the frame whose parsing / handling + * caused the error. + * May be 0 if not related to any specific frame. + * @param errorCode a quic transport error + */ + public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace, + long frameType, QuicTransportErrors errorCode) { + super(reason); + this.keySpace = keySpace; + this.frameType = frameType; + this.errorCode = errorCode.code(); + } + + /** + * Constructs a new {@code QuicTransportException}. For use with TLS alerts. + * + * @param reason the reason why the exception occurred + * @param keySpace the key space in which the frame appeared. + * May be {@code null}, for instance, in + * case of {@link QuicTransportErrors#INTERNAL_ERROR}. + * @param frameType the frame type of the frame whose parsing / handling + * caused the error. + * May be 0 if not related to any specific frame. + * @param errorCode a quic transport error code + * @param cause the cause + */ + public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace, + long frameType, long errorCode, Throwable cause) { + super(reason, cause); + this.keySpace = keySpace; + this.frameType = frameType; + this.errorCode = errorCode; + } + + /** + * {@return the reason to include in the {@code ConnectionCloseFrame}} + */ + public String getReason() { + return getMessage(); + } + + /** + * {@return the key space for which the error occurred, or {@code null}} + */ + public QuicTLSEngine.KeySpace getKeySpace() { + return keySpace; + } + + /** + * {@return the frame type for which the error occurred, or 0} + */ + public long getFrameType() { + return frameType; + } + + /** + * {@return the transport error that occurred} + */ + public long getErrorCode() { + return errorCode; + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java new file mode 100644 index 00000000000..9429f6bf26f --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import java.nio.ByteBuffer; + +/** + * Interface for consumer of QUIC transport parameters, in wire-encoded format + */ +public interface QuicTransportParametersConsumer { + /** + * Consumes the provided QUIC transport parameters + * @param buffer byte buffer containing encoded quic transport parameters + * @throws QuicTransportException if buffer does not represent valid parameters + */ + void accept(ByteBuffer buffer) throws QuicTransportException; +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java new file mode 100644 index 00000000000..14bbaba816d --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents the Quic versions defined in their corresponding RFCs + */ +public enum QuicVersion { + // the version numbers are defined in their respective RFCs + QUIC_V1(1), // RFC-9000 + QUIC_V2(0x6b3343cf); // RFC 9369 + + // 32 bits unsigned integer representing the version as + // defined in RFC. This is the version number as sent + // in long headers packets (see RFC 9000). + private final int versionNumber; + + private QuicVersion(final int versionNumber) { + this.versionNumber = versionNumber; + } + + /** + * {@return the version number} + */ + public int versionNumber() { + return this.versionNumber; + } + + /** + * {@return the QuicVersion corresponding to the {@code versionNumber} or + * {@link Optional#empty() an empty Optional} if the {@code versionNumber} + * doesn't correspond to a Quic version} + * + * @param versionNumber The version number + */ + public static Optional of(int versionNumber) { + for (QuicVersion qv : QuicVersion.values()) { + if (qv.versionNumber == versionNumber) { + return Optional.of(qv); + } + } + return Optional.empty(); + } + + /** + * From among the {@code quicVersions}, selects a {@code QuicVersion} to be used in the + * first packet during connection initiation. + * + * @param quicVersions the available QUIC versions + * @return the QUIC version to use in the first packet + * @throws NullPointerException if {@code quicVersions} is null or any element + * in it is null + * @throws IllegalArgumentException if {@code quicVersions} is empty + */ + public static QuicVersion firstFlightVersion(final Collection quicVersions) { + Objects.requireNonNull(quicVersions); + if (quicVersions.isEmpty()) { + throw new IllegalArgumentException("Empty quic versions"); + } + if (quicVersions.size() == 1) { + return quicVersions.iterator().next(); + } + for (final QuicVersion version : quicVersions) { + if (version == QUIC_V1) { + // we always prefer QUIC v1 for first flight version + return QUIC_V1; + } + } + // the given versions did not have QUIC v1, which implies the + // only available first flight version is QUIC v2 + return QUIC_V2; + } +} diff --git a/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java b/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java index d56ecff5e36..be241a1eca5 100644 --- a/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java +++ b/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java @@ -106,13 +106,6 @@ public class ConstantPool { // Internals only below this point // - static { - Reflection.registerFieldsToFilter(ConstantPool.class, Set.of("constantPoolOop")); - } - - // HotSpot-internal constant pool object (set by the VM, name known to the VM) - private Object constantPoolOop; - private native int getSize0 (); private native Class getClassAt0 (int index); private native Class getClassAtIfLoaded0 (int index); diff --git a/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java b/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java index a8d2fe8bb74..46885e12adf 100644 --- a/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java +++ b/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -33,6 +34,10 @@ import jdk.internal.vm.annotation.ForceInline; * @since 24 */ public abstract class ModifiedUtf { + // Maximum number of bytes allowed for a Modified UTF-8 encoded string + // in a ClassFile constant pool entry (CONSTANT_Utf8_info). + public static final int CONSTANT_POOL_UTF8_MAX_BYTES = 65535; + private ModifiedUtf() { } @@ -59,13 +64,34 @@ public abstract class ModifiedUtf { * @param countNonZeroAscii the number of non-zero ascii characters in the prefix calculated by JLA.countNonZeroAscii(str) */ @ForceInline - public static int utfLen(String str, int countNonZeroAscii) { - int utflen = str.length(); - for (int i = utflen - 1; i >= countNonZeroAscii; i--) { + public static long utfLen(String str, int countNonZeroAscii) { + long utflen = str.length(); + for (int i = (int)utflen - 1; i >= countNonZeroAscii; i--) { int c = str.charAt(i); if (c >= 0x80 || c == 0) - utflen += (c >= 0x800) ? 2 : 1; + utflen += (c >= 0x800) ? 2L : 1L; } return utflen; } + + /** + * Checks whether the Modified UTF-8 encoded length of the given string + * fits within the ClassFile constant pool limit (u2 length = 65535 bytes). + * @param str the string to check + */ + @ForceInline + public static boolean isValidLengthInConstantPool(String str) { + // Quick approximation: each char can be at most 3 bytes in Modified UTF-8. + // If the string is short enough, it definitely fits. + int strLen = str.length(); + if (strLen <= CONSTANT_POOL_UTF8_MAX_BYTES / 3) { + return true; + } + if (strLen > CONSTANT_POOL_UTF8_MAX_BYTES) { + return false; + } + // Check exact Modified UTF-8 length. + long utfLen = utfLen(str, 0); + return utfLen <= CONSTANT_POOL_UTF8_MAX_BYTES; + } } diff --git a/src/java.base/share/classes/jdk/internal/vm/Continuation.java b/src/java.base/share/classes/jdk/internal/vm/Continuation.java index a97f9ac9ea4..a7eb3ea6a9f 100644 --- a/src/java.base/share/classes/jdk/internal/vm/Continuation.java +++ b/src/java.base/share/classes/jdk/internal/vm/Continuation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -57,7 +57,6 @@ public class Continuation { /** Reason for pinning */ public enum Pinned { /** Native frame on stack */ NATIVE, - /** Monitor held */ MONITOR, /** In critical section */ CRITICAL_SECTION, /** Exception (OOME/SOE) */ EXCEPTION } @@ -69,8 +68,7 @@ public class Continuation { /** Permanent failure: continuation already yielding */ PERM_FAIL_YIELDING(null), /** Permanent failure: continuation not mounted on the thread */ PERM_FAIL_NOT_MOUNTED(null), /** Transient failure: continuation pinned due to a held CS */ TRANSIENT_FAIL_PINNED_CRITICAL_SECTION(Pinned.CRITICAL_SECTION), - /** Transient failure: continuation pinned due to native frame */ TRANSIENT_FAIL_PINNED_NATIVE(Pinned.NATIVE), - /** Transient failure: continuation pinned due to a held monitor */ TRANSIENT_FAIL_PINNED_MONITOR(Pinned.MONITOR); + /** Transient failure: continuation pinned due to native frame */ TRANSIENT_FAIL_PINNED_NATIVE(Pinned.NATIVE); final Pinned pinned; private PreemptStatus(Pinned reason) { this.pinned = reason; } @@ -85,8 +83,7 @@ public class Continuation { return switch (reason) { case 2 -> Pinned.CRITICAL_SECTION; case 3 -> Pinned.NATIVE; - case 4 -> Pinned.MONITOR; - case 5 -> Pinned.EXCEPTION; + case 4 -> Pinned.EXCEPTION; default -> throw new AssertionError("Unknown pinned reason: " + reason); }; } diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java index a26003a3afb..276c379a564 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java @@ -205,7 +205,10 @@ public class ThreadDumper { // park blocker Object parkBlocker = snapshot.parkBlocker(); if (parkBlocker != null) { - writer.println(" - parking to wait for " + decorateObject(parkBlocker)); + String suffix = (snapshot.parkBlockerOwner() instanceof Thread owner) + ? ", owner #" + owner.threadId() + : ""; + writer.println(" - parking to wait for " + decorateObject(parkBlocker) + suffix); } // blocked on monitor enter or Object.wait @@ -335,6 +338,9 @@ public class ThreadDumper { // parkBlocker is an object to allow for exclusiveOwnerThread in the future jsonWriter.startObject("parkBlocker"); jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker)); + if (snapshot.parkBlockerOwner() instanceof Thread owner) { + jsonWriter.writeProperty("owner", owner.threadId()); + } jsonWriter.endObject(); } diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java index 4fcbaf24d2e..357d38008d1 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java @@ -44,6 +44,8 @@ class ThreadSnapshot { // an object the thread is blocked/waiting on, converted to ThreadBlocker by ThreadSnapshot.of() private int blockerTypeOrdinal; private Object blockerObject; + // the owner of the blockerObject when the object is park blocker and is AbstractOwnableSynchronizer + private Thread parkBlockerOwner; // set by ThreadSnapshot.of() private ThreadBlocker blocker; @@ -70,8 +72,11 @@ class ThreadSnapshot { snapshot.locks = EMPTY_LOCKS; } if (snapshot.blockerObject != null) { - snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, snapshot.blockerObject); + snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, + snapshot.blockerObject, + snapshot.parkBlockerOwner); snapshot.blockerObject = null; // release + snapshot.parkBlockerOwner = null; } return snapshot; } @@ -104,6 +109,13 @@ class ThreadSnapshot { return getBlocker(BlockerLockType.PARK_BLOCKER); } + /** + * Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer. + */ + Thread parkBlockerOwner() { + return (blocker != null && blocker.type == BlockerLockType.PARK_BLOCKER) ? blocker.owner : null; + } + /** * Returns the object that the thread is blocked on. * @throws IllegalStateException if not in the blocked state @@ -211,11 +223,11 @@ class ThreadSnapshot { } } - private record ThreadBlocker(BlockerLockType type, Object obj) { + private record ThreadBlocker(BlockerLockType type, Object obj, Thread owner) { private static final BlockerLockType[] lockTypeValues = BlockerLockType.values(); // cache - ThreadBlocker(int typeOrdinal, Object obj) { - this(lockTypeValues[typeOrdinal], obj); + ThreadBlocker(int typeOrdinal, Object obj, Thread owner) { + this(lockTypeValues[typeOrdinal], obj, owner); } } diff --git a/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java b/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java index 3c2dab3209a..1305a7b6b88 100644 --- a/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java +++ b/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java @@ -30,25 +30,34 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/// Indicates that the static initializer of this class or interface -/// (its `` method) is allowed to be _AOT-initialized_, -/// because its author considers it safe to execute during the AOT -/// assembly phase. +/// Indicates that the annotated class or interface is allowed to be _AOT-initialized_, +/// because its author considers it safe to execute the static initializer of +/// the class or interface during the AOT assembly phase. /// -/// This annotation directs the VM to expect that normal execution of Java code -/// during the assembly phase could trigger initialization of this class, -/// and if that happens, to store the resulting static field values in the -/// AOT cache. (These fields happen to be allocated in the `Class` mirror.) +/// For a class or interface _X_ annotated with `@AOTSafeClassInitializer`, it will +/// be initialized in the AOT assembly phase under two circumstances: /// -/// During the production run, the static initializer (``) of -/// this class or interface will not be executed, if it was already -/// executed during the assembling of the AOT being used to start the -/// production run. In that case the resulting static field states -/// (within the `Class` mirror) were already stored in the AOT cache. +/// 1. If _X_ was initialized during the AOT training run, the JVM will proactively +/// initialize _X_ in the assembly phase. +/// 2. If _X_ was not initialized during the AOT training run, the initialization of +/// _X_ can still be triggered by normal execution of Java code in the assembly +/// phase. At present this is usually the result of performing AOT optimizations for +/// the `java.lang.invoke` package but it may include other cases as well. /// -/// Currently, this annotation is used mainly for supporting AOT -/// linking of APIs, including bootstrap methods, in the -/// `java.lang.invoke` package. +/// If _X_ is initialized during the AOT assembly phase, the VM will store +/// the values of the static fields of _X_ in the AOT cache. Consequently, +/// during the production run that uses this AOT cache, the static initializer +/// (``) of _X_ will not be executed. _X_ will appear to be in the +/// "initialized" state and all the cached values of the static field of _X_ +/// will be available immediately upon the start of the prodcution run. +/// +/// Currently, this annotation is used mainly for two purposes: +/// +/// - To AOT-initialize complex static fields whose values are always the same +/// across JVM lifetimes. One example is the tables of constant values +/// in the `jdk.internal.math.MathUtils` class. +/// - To support AOT linking of APIs, including bootstrap methods, in the +/// `java.lang.invoke` package. /// /// In more detail, the AOT assembly phase performs the following: /// @@ -62,6 +71,8 @@ import java.lang.annotation.Target; /// along with every relevant superclass and implemented interface, along /// with classes for every object created during the course of static /// initialization (running `` for each such class or interface). +/// 5. In addition, any class/interface annotated with `@AOTSafeClassInitializer` +/// that was initialized during the training run is proactively initialized. /// /// Thus, in order to determine that a class or interface _X_ is safe to /// AOT-initialize requires evaluating every other class or interface _Y_ that @@ -112,21 +123,18 @@ import java.lang.annotation.Target; /// remotely) if the execution of such an API touches _X_ for initialization, /// or even if such an API request is in any way sensitive to values stored in /// the fields of _X_, even if the sensitivity is a simple reference identity -/// test. As noted above, all supertypes of _X_ must also have the -/// `@AOTSafeClassInitializer` annotation, and must also be safe for AOT -/// initialization. +/// test. /// /// The author of an AOT-initialized class may elect to patch some states at /// production startup, using an [AOTRuntimeSetup] method, as long as the /// pre-patched field values (present during AOT assembly) are determined to be /// compatible with the post-patched values that apply to the production run. /// -/// In the assembly phase, `classFileParser.cpp` performs checks on the annotated -/// classes, to ensure all supertypes of this class that must be initialized when -/// this class is initialized have the `@AOTSafeClassInitializer` annotation. -/// Otherwise, a [ClassFormatError] will be thrown. (This assembly phase restriction -/// allows module patching and instrumentation to work on annotated classes when -/// AOT is not enabled) +/// Before adding this annotation to a class _X_, the author must determine +/// that it's safe to execute the static initializer of _X_ during the AOT +/// assembly phase. In addition, all supertypes of _X_ must also have this +/// annotation. If a supertype of _X_ is found to be missing this annotation, +/// the AOT assembly phase will fail. /// /// This annotation is only recognized on privileged code and is ignored elsewhere. /// diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index 2a51a0af38d..3ae84fdf198 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -190,6 +190,8 @@ module java.base { jdk.jlink; exports jdk.internal.logger to java.logging; + exports jdk.internal.net.quic to + java.net.http; exports jdk.internal.org.xml.sax to jdk.jfr; exports jdk.internal.org.xml.sax.helpers to @@ -260,6 +262,7 @@ module java.base { jdk.jfr; exports jdk.internal.util to java.desktop, + java.net.http, java.prefs, java.security.jgss, java.smartcardio, diff --git a/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java b/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java index 76bbff2a610..bd5ea6d7635 100644 --- a/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java +++ b/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java @@ -37,12 +37,33 @@ public class BytecodeDescriptor { private BytecodeDescriptor() { } // cannot instantiate - /** - * @param loader the class loader in which to look up the types (null means - * bootstrap class loader) - */ - public static List> parseMethod(String bytecodeSignature, ClassLoader loader) { - return parseMethod(bytecodeSignature, 0, bytecodeSignature.length(), loader); + /// Parses and validates a field descriptor string in the {@code loader} context. + /// + /// @param descriptor a field descriptor string + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// @throws IllegalArgumentException if the descriptor is invalid + /// @throws TypeNotPresentException if the descriptor is valid, but + /// the class cannot be found by the loader + public static Class parseClass(String descriptor, ClassLoader loader) { + int[] i = {0}; + var ret = parseSig(descriptor, i, descriptor.length(), loader); + if (i[0] != descriptor.length() || ret == null) { + parseError("not a class descriptor", descriptor); + } + return ret; + } + + /// Parses and validates a method descriptor string in the {@code loader} context. + /// + /// @param descriptor a method descriptor string + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// @throws IllegalArgumentException if the descriptor is invalid + /// @throws TypeNotPresentException if a reference type cannot be found by + /// the loader (before the descriptor is found invalid) + public static List> parseMethod(String descriptor, ClassLoader loader) { + return parseMethod(descriptor, 0, descriptor.length(), loader); } /** @@ -77,10 +98,19 @@ public class BytecodeDescriptor { throw new IllegalArgumentException("bad signature: "+str+": "+msg); } - /** - * @param loader the class loader in which to look up the types (null means - * bootstrap class loader) - */ + /// Parse a single type in a descriptor. Results can be: + /// + /// - A `Class` for successful parsing + /// - `null` for malformed descriptor format + /// - Throwing a [TypeNotPresentException] for valid class name, + /// but class cannot be found + /// + /// @param str contains the string to parse + /// @param i cursor for the next token in the string, modified in-place + /// @param end the limit for parsing + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// private static Class parseSig(String str, int[] i, int end, ClassLoader loader) { if (i[0] == end) return null; char c = str.charAt(i[0]++); @@ -107,7 +137,14 @@ public class BytecodeDescriptor { } return t; } else { - return Wrapper.forBasicType(c).primitiveType(); + Wrapper w; + try { + w = Wrapper.forBasicType(c); + } catch (IllegalArgumentException ex) { + // Our reporting has better error message + return null; + } + return w.primitiveType(); } } diff --git a/src/java.base/share/classes/sun/launcher/SecuritySettings.java b/src/java.base/share/classes/sun/launcher/SecuritySettings.java index afd5ead4914..e7ec70afcc3 100644 --- a/src/java.base/share/classes/sun/launcher/SecuritySettings.java +++ b/src/java.base/share/classes/sun/launcher/SecuritySettings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -136,7 +136,18 @@ public final class SecuritySettings { for (String s : ssls.getEnabledCipherSuites()) { ostream.println(THREEINDENT + s); } + + ostream.println("\n" + TWOINDENT + "Enabled Named Groups:"); + String [] groups = ssls.getSSLParameters().getNamedGroups(); + if (groups == null) { + ostream.println(THREEINDENT + ""); + } else { + for (String s : groups) { + ostream.println(THREEINDENT + s); + } + } } + ostream.println(); } diff --git a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java index 20b735fbdf3..71080950b80 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java @@ -87,8 +87,8 @@ public class JavaRuntimeURLConnection extends URLConnection { if (module.isEmpty() || path == null) { throw new IOException("cannot connect to jrt:/" + module); } - Node node = READER.findNode("/modules/" + module + "/" + path); - if (node == null || !node.isResource()) { + Node node = READER.findResourceNode(module, path); + if (node == null) { throw new IOException(module + "/" + path + " not found"); } this.resourceNode = node; diff --git a/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java b/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java index 82751e3fcd2..74ed1e01553 100644 --- a/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java +++ b/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,12 +34,7 @@ import java.util.function.Supplier; import jdk.internal.reflect.ConstantPool; -import sun.reflect.generics.parser.SignatureParser; -import sun.reflect.generics.tree.TypeSignature; -import sun.reflect.generics.factory.GenericsFactory; -import sun.reflect.generics.factory.CoreReflectionFactory; -import sun.reflect.generics.visitor.Reifier; -import sun.reflect.generics.scope.ClassScope; +import sun.invoke.util.BytecodeDescriptor; /** * Parser for Java programming language annotations. Translates @@ -429,19 +424,11 @@ public class AnnotationParser { } private static Class parseSig(String sig, Class container) { - if (sig.equals("V")) return void.class; - SignatureParser parser = SignatureParser.make(); - TypeSignature typeSig = parser.parseTypeSig(sig); - GenericsFactory factory = CoreReflectionFactory.make(container, ClassScope.make(container)); - Reifier reify = Reifier.make(factory); - typeSig.accept(reify); - Type result = reify.getResult(); - return toClass(result); - } - static Class toClass(Type o) { - if (o instanceof GenericArrayType gat) - return toClass(gat.getGenericComponentType()).arrayType(); - return (Class) o; + try { + return BytecodeDescriptor.parseClass(sig, container.getClassLoader()); + } catch (IllegalArgumentException ex) { + throw new GenericSignatureFormatError(ex.getMessage()); + } } /** diff --git a/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java b/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java index f127ac65cc7..4acc0833c5d 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java @@ -221,7 +221,7 @@ public final class AlgorithmChecker extends PKIXCertPathChecker { currSigAlg, prevPubKey, currSigAlgParams)) { throw new CertPathValidatorException( "Algorithm constraints check failed on " + - currSigAlg + "signature and " + + currSigAlg + " signature and " + currPubKey.getAlgorithm() + " key with size of " + sun.security.util.KeyUtil.getKeySize(currPubKey) + "bits", diff --git a/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java b/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java index 270d10e82fa..397e8093cf7 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -212,6 +212,11 @@ public final class PKIXCertPathValidator extends CertPathValidatorSpi { ((RevocationChecker)checker).init(anchor, params); } } + + // Set trust anchor for the user-specified AlgorithmChecker. + if (checker instanceof AlgorithmChecker algChecker) { + algChecker.trySetTrustAnchor(anchor); + } } // only add a RevocationChecker if revocation is enabled and // a PKIXRevocationChecker has not already been added diff --git a/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java b/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java index c4e31cf7947..8b47c437dac 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -637,18 +637,4 @@ public final class SunCertPathBuilder extends CertPathBuilderSpi { return (nextAltNameExt == null); } } - - /** - * Returns true if trust anchor certificate matches specified - * certificate constraints. - */ - private static boolean anchorIsTarget(TrustAnchor anchor, - CertSelector sel) - { - X509Certificate anchorCert = anchor.getTrustedCert(); - if (anchorCert != null) { - return sel.match(anchorCert); - } - return false; - } } diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java index 4e1ccf385c7..ed5e079bf44 100644 --- a/src/java.base/share/classes/sun/security/ssl/Alert.java +++ b/src/java.base/share/classes/sun/security/ssl/Alert.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,9 +34,9 @@ import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLProtocolException; /** - * SSL/(D)TLS Alter description + * SSL/(D)TLS Alert description */ -enum Alert { +public enum Alert { // Please refer to TLS Alert Registry for the latest (D)TLS Alert values: // https://www.iana.org/assignments/tls-parameters/ CLOSE_NOTIFY ((byte)0, "close_notify", false), @@ -103,7 +103,7 @@ enum Alert { return null; } - static String nameOf(byte id) { + public static String nameOf(byte id) { for (Alert al : Alert.values()) { if (al.id == id) { return al.description; @@ -181,6 +181,16 @@ enum Alert { AlertMessage(TransportContext context, ByteBuffer m) throws IOException { + // From RFC 8446 "Implementations + // MUST NOT send Handshake and Alert records that have a zero-length + // TLSInnerPlaintext.content; if such a message is received, the + // receiving implementation MUST terminate the connection with an + // "unexpected_message" alert." + if (m.remaining() == 0) { + throw context.fatal(Alert.UNEXPECTED_MESSAGE, + "Alert fragments must not be zero length."); + } + // struct { // AlertLevel level; // AlertDescription description; diff --git a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java index d44ec034411..aa5933ddab0 100644 --- a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -71,7 +71,7 @@ final class AlpnExtension { /** * The "application_layer_protocol_negotiation" extension. - * + *

    * See RFC 7301 for the specification of this extension. */ static final class AlpnSpec implements SSLExtensionSpec { @@ -344,6 +344,13 @@ final class AlpnExtension { // The producing happens in server side only. ServerHandshakeContext shc = (ServerHandshakeContext)context; + if (shc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST use ALPN + throw shc.conContext.fatal( + Alert.NO_APPLICATION_PROTOCOL, + "Client did not offer application layer protocol"); + } + // Please don't use the previous negotiated application protocol. shc.applicationProtocol = ""; shc.conContext.applicationProtocol = ""; @@ -513,6 +520,15 @@ final class AlpnExtension { // The producing happens in client side only. ClientHandshakeContext chc = (ClientHandshakeContext)context; + if (chc.sslConfig.isQuic) { + // RFC 9001: QUIC clients MUST use error 0x0178 + // [no_application_protocol] to terminate a connection when + // ALPN negotiation fails + throw chc.conContext.fatal( + Alert.NO_APPLICATION_PROTOCOL, + "Server did not offer application layer protocol"); + } + // Please don't use the previous negotiated application protocol. chc.applicationProtocol = ""; chc.conContext.applicationProtocol = ""; diff --git a/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java b/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java index b975290d09d..2125a148162 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java @@ -36,6 +36,11 @@ import sun.security.ssl.SignatureAlgorithmsExtension.SignatureSchemesSpec; /** * Pack of the "signature_algorithms_cert" extensions. + *

    + * Note: Per RFC 8446, if no "signature_algorithms_cert" extension is + * present, then the "signature_algorithms" extension also applies to + * signatures appearing in certificates. + * See {@code SignatureAlgorithmsExtension} for details. */ final class CertSignAlgsExtension { static final HandshakeProducer chNetworkProducer = diff --git a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java index 609a81571ed..d4587d35ae9 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java +++ b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java @@ -1219,12 +1219,19 @@ final class CertificateMessage { certs.clone(), authType, engine); - } else { - SSLSocket socket = (SSLSocket)shc.conContext.transport; + } else if (shc.conContext.transport instanceof SSLSocket socket){ ((X509ExtendedTrustManager)tm).checkClientTrusted( certs.clone(), authType, socket); + } else if (shc.conContext.transport + instanceof QuicTLSEngineImpl qtlse) { + if (tm instanceof X509TrustManagerImpl tmImpl) { + tmImpl.checkClientTrusted(certs.clone(), authType, qtlse); + } else { + throw new CertificateException( + "QUIC only supports SunJSSE trust managers"); + } } } else { // Unlikely to happen, because we have wrapped the old @@ -1268,18 +1275,26 @@ final class CertificateMessage { try { X509TrustManager tm = chc.sslContext.getX509TrustManager(); - if (tm instanceof X509ExtendedTrustManager) { + if (tm instanceof X509ExtendedTrustManager x509ExtTm) { if (chc.conContext.transport instanceof SSLEngine engine) { - ((X509ExtendedTrustManager)tm).checkServerTrusted( + x509ExtTm.checkServerTrusted( certs.clone(), authType, engine); - } else { - SSLSocket socket = (SSLSocket)chc.conContext.transport; - ((X509ExtendedTrustManager)tm).checkServerTrusted( + } else if (chc.conContext.transport instanceof SSLSocket socket) { + x509ExtTm.checkServerTrusted( certs.clone(), authType, socket); + } else if (chc.conContext.transport instanceof QuicTLSEngineImpl qtlse) { + if (x509ExtTm instanceof X509TrustManagerImpl tmImpl) { + tmImpl.checkServerTrusted(certs.clone(), authType, qtlse); + } else { + throw new CertificateException( + "QUIC only supports SunJSSE trust managers"); + } + } else { + throw new AssertionError("Unexpected transport type"); } } else { // Unlikely to happen, because we have wrapped the old diff --git a/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java b/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java index 6da6f745691..4ea61161c1d 100644 --- a/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java +++ b/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java @@ -232,7 +232,8 @@ final class ChangeCipherSpec { tc.consumers.remove(ContentType.CHANGE_CIPHER_SPEC.id); // parse - if (message.remaining() != 1 || message.get() != 1) { + if (message.remaining() != 1 || message.get() != 1 + || tc.isNegotiated) { throw tc.fatal(Alert.UNEXPECTED_MESSAGE, "Malformed or unexpected ChangeCipherSpec message"); } diff --git a/src/java.base/share/classes/sun/security/ssl/ClientHello.java b/src/java.base/share/classes/sun/security/ssl/ClientHello.java index 3e43921520d..c9432ea3979 100644 --- a/src/java.base/share/classes/sun/security/ssl/ClientHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ClientHello.java @@ -568,7 +568,7 @@ final class ClientHello { } if (sessionId.length() == 0 && chc.maximumActiveProtocol.useTLS13PlusSpec() && - SSLConfiguration.useCompatibilityMode) { + chc.sslConfig.isUseCompatibilityMode()) { // In compatibility mode, the TLS 1.3 legacy_session_id // field MUST be non-empty, so a client not offering a // pre-TLS 1.3 session MUST generate a new 32-byte value. diff --git a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java index e0196f3009c..101a42a5407 100644 --- a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -801,8 +801,11 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // buffer this fragment if (hsf.handshakeType == SSLHandshake.FINISHED.id) { - // Need no status update. - bufferedFragments.add(hsf); + // Make sure it's not a retransmitted message + if (hsf.recordEpoch > handshakeEpoch) { + bufferedFragments.add(hsf); + flightIsReady = holes.isEmpty(); + } } else { bufferFragment(hsf); } diff --git a/src/java.base/share/classes/sun/security/ssl/Finished.java b/src/java.base/share/classes/sun/security/ssl/Finished.java index 9421d12ec15..04fe61760d0 100644 --- a/src/java.base/share/classes/sun/security/ssl/Finished.java +++ b/src/java.base/share/classes/sun/security/ssl/Finished.java @@ -846,6 +846,16 @@ final class Finished { // update the context for the following key derivation shc.handshakeKeyDerivation = secretKD; + if (shc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + engine.deriveOneRTTKeys(); + } catch (IOException e) { + throw shc.conContext.fatal(Alert.INTERNAL_ERROR, + "Failure to derive application secrets", e); + } + } } catch (GeneralSecurityException gse) { throw shc.conContext.fatal(Alert.INTERNAL_ERROR, "Failure to derive application secrets", gse); @@ -1010,6 +1020,16 @@ final class Finished { // update the context for the following key derivation chc.handshakeKeyDerivation = secretKD; + if (chc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + engine.deriveOneRTTKeys(); + } catch (IOException e) { + throw chc.conContext.fatal(Alert.INTERNAL_ERROR, + "Failure to derive application secrets", e); + } + } } catch (GeneralSecurityException gse) { throw chc.conContext.fatal(Alert.INTERNAL_ERROR, "Failure to derive application secrets", gse); diff --git a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java index c4549070f02..2b17c7406a3 100644 --- a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java +++ b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java @@ -269,6 +269,12 @@ final class KeyUpdate { HandshakeMessage message) throws IOException { // The producing happens in server side only. PostHandshakeContext hc = (PostHandshakeContext)context; + if (hc.sslConfig.isQuic) { + // Quic doesn't allow KEY_UPDATE TLS message. It has its own Quic specific + // key update mechanism, RFC-9001, section 6: + // Endpoints MUST NOT send a TLS KeyUpdate message. + return null; + } KeyUpdateMessage km = (KeyUpdateMessage)message; if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { SSLLogger.fine( diff --git a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java index 0fa831f6351..f2c30b3ff72 100644 --- a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,6 +31,8 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.quic.QuicTLSEngine; import sun.security.ssl.SSLCipher.SSLWriteCipher; /** @@ -154,6 +156,16 @@ abstract class OutputRecord throw new UnsupportedOperationException(); } + // apply to QuicEngine only + byte[] getHandshakeMessage() { + throw new UnsupportedOperationException(); + } + + // apply to QuicEngine only + QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() { + throw new UnsupportedOperationException(); + } + // apply to SSLEngine only void encodeV2NoCipher() throws IOException { throw new UnsupportedOperationException(); diff --git a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java index b06549b40e3..a4f87616245 100644 --- a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java +++ b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java @@ -47,17 +47,15 @@ final class PostHandshakeContext extends HandshakeContext { context.conSession.getLocalSupportedSignatureSchemes()); // Add the potential post-handshake consumers. - if (context.sslConfig.isClientMode) { + if (!context.sslConfig.isQuic) { handshakeConsumers.putIfAbsent( SSLHandshake.KEY_UPDATE.id, SSLHandshake.KEY_UPDATE); + } + if (context.sslConfig.isClientMode) { handshakeConsumers.putIfAbsent( SSLHandshake.NEW_SESSION_TICKET.id, SSLHandshake.NEW_SESSION_TICKET); - } else { - handshakeConsumers.putIfAbsent( - SSLHandshake.KEY_UPDATE.id, - SSLHandshake.KEY_UPDATE); } handshakeFinished = true; @@ -93,6 +91,15 @@ final class PostHandshakeContext extends HandshakeContext { static boolean isConsumable(TransportContext context, byte handshakeType) { if (handshakeType == SSLHandshake.KEY_UPDATE.id) { + // Quic doesn't allow KEY_UPDATE TLS message. It has its own + // Quic-specific key update mechanism, RFC-9001, section 6: + // Endpoints MUST NOT send a TLS KeyUpdate message. Endpoints + // MUST treat the receipt of a TLS KeyUpdate message as a + // connection error of type 0x010a, equivalent to a fatal + // TLS alert of unexpected_message; + if (context.sslConfig.isQuic) { + return false; + } // The KeyUpdate handshake message does not apply to TLS 1.2 and // previous protocols. return context.protocolVersion.useTLS13PlusSpec(); diff --git a/src/java.base/share/classes/sun/security/ssl/QuicCipher.java b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java new file mode 100644 index 00000000000..c1d812e4c40 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java @@ -0,0 +1,699 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; +import java.security.Security; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import sun.security.util.KeyUtil; + +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT; +import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR; + +abstract class QuicCipher { + private static final String + SEC_PROP_QUIC_TLS_KEY_LIMITS = "jdk.quic.tls.keyLimits"; + + private static final Map KEY_LIMITS; + + static { + final String propVal = Security.getProperty( + SEC_PROP_QUIC_TLS_KEY_LIMITS); + if (propVal == null) { + KEY_LIMITS = Map.of(); // no specific limits + } else { + final Map limits = new HashMap<>(); + for (final String entry : propVal.split(",")) { + // each entry is of the form + // example: + // AES/GCM/NoPadding 2^23 + // ChaCha20-Poly1305 -1 + final String[] parts = entry.trim().split(" "); + if (parts.length != 2) { + // TODO: exception type + throw new RuntimeException("invalid value for " + + SEC_PROP_QUIC_TLS_KEY_LIMITS + + " security property"); + } + final String cipher = parts[0]; + if (limits.containsKey(cipher)) { + throw new RuntimeException( + "key limit defined more than once for cipher " + + cipher); + } + final String limitVal = parts[1]; + final long limit; + final int index = limitVal.indexOf("^"); + if (index >= 0) { + // of the form x^y (example: 2^23) + limit = (long) Math.pow( + Integer.parseInt(limitVal.substring(0, index)), + Integer.parseInt(limitVal.substring(index + 1))); + } else { + limit = Long.parseLong(limitVal); + } + if (limit == 0 || limit < -1) { + // we allow -1 to imply no limits, but any other zero + // or negative value is invalid + // TODO: exception type + throw new RuntimeException("invalid value for " + + SEC_PROP_QUIC_TLS_KEY_LIMITS + + " security property"); + } + limits.put(cipher, limit); + } + KEY_LIMITS = Collections.unmodifiableMap(limits); + } + } + + private final CipherSuite cipherSuite; + private final QuicHeaderProtectionCipher hpCipher; + private final SecretKey baseSecret; + private final int keyPhase; + + protected QuicCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final QuicHeaderProtectionCipher hpCipher, final int keyPhase) { + assert keyPhase == 0 || keyPhase == 1 : + "invalid key phase: " + keyPhase; + this.cipherSuite = cipherSuite; + this.baseSecret = baseSecret; + this.hpCipher = hpCipher; + this.keyPhase = keyPhase; + } + + final SecretKey getBaseSecret() { + return this.baseSecret; + } + + final CipherSuite getCipherSuite() { + return this.cipherSuite; + } + + final SecretKey getHeaderProtectionKey() { + return this.hpCipher.headerProtectionKey; + } + + final ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + return hpCipher.computeHeaderProtectionMask(sample); + } + + final int getKeyPhase() { + return this.keyPhase; + } + + final void discard(boolean destroyHP) { + safeDiscard(this.baseSecret); + if (destroyHP) { + this.hpCipher.discard(); + } + this.doDiscard(); + } + + protected abstract void doDiscard(); + + static QuicReadCipher createReadCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + return switch (cipherSuite) { + case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 -> + new T13GCMReadCipher( + cipherSuite, baseSecret, key, iv, hp, keyPhase); + case TLS_CHACHA20_POLY1305_SHA256 -> + new T13CC20P1305ReadCipher( + cipherSuite, baseSecret, key, iv, hp, keyPhase); + default -> throw new IllegalArgumentException("Cipher suite " + + cipherSuite + " not supported"); + }; + } + + static QuicWriteCipher createWriteCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + return switch (cipherSuite) { + case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 -> + new T13GCMWriteCipher(cipherSuite, baseSecret, key, iv, hp, + keyPhase); + case TLS_CHACHA20_POLY1305_SHA256 -> + new T13CC20P1305WriteCipher(cipherSuite, baseSecret, key, iv, + hp, keyPhase); + default -> throw new IllegalArgumentException("Cipher suite " + + cipherSuite + " not supported"); + }; + } + + static void safeDiscard(final SecretKey secretKey) { + KeyUtil.destroySecretKeys(secretKey); + } + + abstract static class QuicReadCipher extends QuicCipher { + private final AtomicLong lowestDecryptedPktNum = new AtomicLong(-1); + + QuicReadCipher(CipherSuite cipherSuite, SecretKey baseSecret, + QuicHeaderProtectionCipher hpCipher, int keyPhase) { + super(cipherSuite, baseSecret, hpCipher, keyPhase); + } + + final void decryptPacket(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + doDecrypt(packetNumber, packet, headerLength, output); + boolean updated; + do { + final long current = lowestDecryptedPktNum.get(); + assert packetNumber >= 0 : + "unexpected packet number: " + packetNumber; + final long newLowest = current == -1 ? packetNumber : + Math.min(current, packetNumber); + updated = lowestDecryptedPktNum.compareAndSet(current, + newLowest); + } while (!updated); + } + + protected abstract void doDecrypt(long packetNumber, + ByteBuffer packet, int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException; + + /** + * Returns the maximum limit on the number of packets that fail + * decryption, across all key (updates), using this + * {@code QuicReadCipher}. This method must not return a value less + * than 0. + * + * @return the limit + */ + // RFC-9001, section 6.6 + abstract long integrityLimit(); + + /** + * {@return the lowest packet number that this {@code QuicReadCipher} + * has decrypted. If no packets have yet been decrypted by this + * instance, then this method returns -1} + */ + final long lowestDecryptedPktNum() { + return this.lowestDecryptedPktNum.get(); + } + + /** + * {@return true if this {@code QuicReadCipher} has successfully + * decrypted any packet sent by the peer, else returns false} + */ + final boolean hasDecryptedAny() { + return this.lowestDecryptedPktNum.get() != -1; + } + } + + abstract static class QuicWriteCipher extends QuicCipher { + private final AtomicLong numPacketsEncrypted = new AtomicLong(); + private final AtomicLong lowestEncryptedPktNum = new AtomicLong(-1); + + QuicWriteCipher(CipherSuite cipherSuite, SecretKey baseSecret, + QuicHeaderProtectionCipher hpCipher, int keyPhase) { + super(cipherSuite, baseSecret, hpCipher, keyPhase); + } + + final void encryptPacket(final long packetNumber, + final ByteBuffer packetHeader, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicTransportException, ShortBufferException { + final long confidentialityLimit = confidentialityLimit(); + final long numEncrypted = this.numPacketsEncrypted.get(); + if (confidentialityLimit > 0 && + numEncrypted > confidentialityLimit) { + // the OneRttKeyManager is responsible for detecting and + // initiating a key update before this limit is hit. The fact + // that we hit this limit indicates that either the key + // update wasn't initiated or the key update failed. In + // either case we just throw an exception which + // should lead to the connection being closed as required by + // RFC-9001, section 6.6: + // If a key update is not possible or integrity limits are + // reached, the endpoint MUST stop using the connection and + // only send stateless resets in response to receiving + // packets. It is RECOMMENDED that endpoints immediately + // close the connection with a connection error of type + // AEAD_LIMIT_REACHED before reaching a state where key + // updates are not possible. + throw new QuicTransportException("confidentiality limit " + + "reached", ONE_RTT, 0, + QuicTransportErrors.AEAD_LIMIT_REACHED); + } + this.numPacketsEncrypted.incrementAndGet(); + doEncryptPacket(packetNumber, packetHeader, packetPayload, output); + boolean updated; + do { + final long current = lowestEncryptedPktNum.get(); + assert packetNumber >= 0 : + "unexpected packet number: " + packetNumber; + final long newLowest = current == -1 ? packetNumber : + Math.min(current, packetNumber); + updated = lowestEncryptedPktNum.compareAndSet(current, + newLowest); + } while (!updated); + } + + /** + * {@return the lowest packet number that this {@code QuicWriteCipher} + * has encrypted. If no packets have yet been encrypted by this + * instance, then this method returns -1} + */ + final long lowestEncryptedPktNum() { + return this.lowestEncryptedPktNum.get(); + } + + /** + * {@return true if this {@code QuicWriteCipher} has successfully + * encrypted any packet to send to the peer, else returns false} + */ + final boolean hasEncryptedAny() { + // rely on the lowestEncryptedPktNum field instead of the + // numPacketsEncrypted field. this avoids a race where the + // lowestEncryptedPktNum() might return a value contradicting + // the return value of this method. + return this.lowestEncryptedPktNum.get() != -1; + } + + /** + * {@return the number of packets encrypted by this {@code + * QuicWriteCipher}} + */ + final long getNumEncrypted() { + return this.numPacketsEncrypted.get(); + } + + abstract void doEncryptPacket(long packetNumber, ByteBuffer packetHeader, + ByteBuffer packetPayload, ByteBuffer output) + throws ShortBufferException, QuicTransportException; + + /** + * Returns the maximum limit on the number of packets that are allowed + * to be encrypted with this instance of {@code QuicWriteCipher}. A + * value less than 0 implies that there's no limit. + * + * @return the limit or -1 + */ + // RFC-9001, section 6.6: The confidentiality limit applies to the + // number of + // packets encrypted with a given key. + abstract long confidentialityLimit(); + } + + abstract static class QuicHeaderProtectionCipher { + protected final SecretKey headerProtectionKey; + + protected QuicHeaderProtectionCipher( + final SecretKey headerProtectionKey) { + this.headerProtectionKey = headerProtectionKey; + } + + int getHeaderProtectionSampleSize() { + return 16; + } + + abstract ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException; + + final void discard() { + safeDiscard(this.headerProtectionKey); + } + } + + static final class T13GCMReadCipher extends QuicReadCipher { + // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM, + // the integrity limit is 2^52 invalid packets + private static final long INTEGRITY_LIMIT = 1L << 52; + + private final Cipher cipher; + private final SecretKey key; + private final byte[] iv; + + T13GCMReadCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final SecretKey key, final byte[] iv, final SecretKey hp, + final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance("AES/GCM/NoPadding"); + } + + @Override + protected void doDecrypt(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv); + synchronized (cipher) { + try { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + int limit = packet.limit(); + packet.limit(packet.position() + headerLength); + cipher.updateAAD(packet); + packet.limit(limit); + cipher.doFinal(packet, output); + } catch (AEADBadTagException | ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Decryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long integrityLimit() { + return INTEGRITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13GCMWriteCipher extends QuicWriteCipher { + private static final String CIPHER_ALGORITHM_NAME = "AES/GCM/NoPadding"; + private static final long CONFIDENTIALITY_LIMIT; + + static { + // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM, + // the confidentiality limit is 2^23 encrypted packets + final long defaultVal = 1 << 23; + long limit = + KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal); + // don't allow the configuration to increase the confidentiality + // limit, but only let it lower the limit + limit = limit > defaultVal ? defaultVal : limit; + CONFIDENTIALITY_LIMIT = limit; + } + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13GCMWriteCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final SecretKey key, final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME); + } + + @Override + void doEncryptPacket(long packetNumber, ByteBuffer packetHeader, + ByteBuffer packetPayload, ByteBuffer output) + throws ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv); + synchronized (cipher) { + try { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + cipher.updateAAD(packetHeader); + cipher.doFinal(packetPayload, output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long confidentialityLimit() { + return CONFIDENTIALITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13AESHPCipher extends QuicHeaderProtectionCipher { + private final Cipher cipher; + + T13AESHPCipher(SecretKey hp) throws GeneralSecurityException { + super(hp); + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } + + @Override + public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + if (sample.remaining() != getHeaderProtectionSampleSize()) { + throw new IllegalArgumentException("Invalid sample size"); + } + ByteBuffer output = ByteBuffer.allocate(sample.remaining()); + try { + synchronized (cipher) { + // Some providers (Jipher) don't re-initialize the cipher + // after doFinal, and need init every time. + cipher.init(Cipher.ENCRYPT_MODE, headerProtectionKey); + cipher.doFinal(sample, output); + } + output.flip(); + assert output.remaining() >= 5; + return output; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + static final class T13CC20P1305ReadCipher extends QuicReadCipher { + // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, + // the integrity limit is 2^36 invalid packets + private static final long INTEGRITY_LIMIT = 1L << 36; + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13CC20P1305ReadCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance("ChaCha20-Poly1305"); + } + + @Override + protected void doDecrypt(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + synchronized (cipher) { + try { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + int limit = packet.limit(); + packet.limit(packet.position() + headerLength); + cipher.updateAAD(packet); + packet.limit(limit); + cipher.doFinal(packet, output); + } catch (AEADBadTagException | ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Decryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long integrityLimit() { + return INTEGRITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13CC20P1305WriteCipher extends QuicWriteCipher { + private static final String CIPHER_ALGORITHM_NAME = "ChaCha20-Poly1305"; + private static final long CONFIDENTIALITY_LIMIT; + + static { + // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, the + // confidentiality limit is greater than the number of possible + // packets (2^62) and so can be disregarded. + final long defaultVal = -1; // no limit + long limit = + KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal); + limit = limit < 0 ? -1 /* no limit */ : limit; + CONFIDENTIALITY_LIMIT = limit; + } + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13CC20P1305WriteCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME); + } + + @Override + void doEncryptPacket(final long packetNumber, final ByteBuffer packetHeader, + final ByteBuffer packetPayload, final ByteBuffer output) + throws ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + long pn = packetNumber; + while (pn > 0) { + iv[i] ^= (byte) (pn & 0xFF); + pn = pn >>> 8; + i--; + } + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + synchronized (cipher) { + try { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + cipher.updateAAD(packetHeader); + cipher.doFinal(packetPayload, output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long confidentialityLimit() { + return CONFIDENTIALITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13CC20HPCipher extends QuicHeaderProtectionCipher { + private final Cipher cipher; + + T13CC20HPCipher(final SecretKey hp) throws GeneralSecurityException { + super(hp); + cipher = Cipher.getInstance("ChaCha20"); + } + + @Override + public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + if (sample.remaining() != getHeaderProtectionSampleSize()) { + throw new IllegalArgumentException("Invalid sample size"); + } + try { + // RFC 7539: [counter is a] 32-bit block count parameter, + // treated as a 32-bit little-endian integer + // RFC 9001: + // counter = sample[0..3] + // nonce = sample[4..15] + // mask = ChaCha20(hp_key, counter, nonce, {0,0,0,0,0}) + + sample.order(ByteOrder.LITTLE_ENDIAN); + byte[] nonce = new byte[12]; + int counter = sample.getInt(); + sample.get(nonce); + ChaCha20ParameterSpec ivSpec = + new ChaCha20ParameterSpec(nonce, counter); + byte[] output = new byte[5]; + + synchronized (cipher) { + // DECRYPT produces the same output as ENCRYPT, but does + // not throw when the same IV is used repeatedly + cipher.init(Cipher.DECRYPT_MODE, headerProtectionKey, + ivSpec); + int numBytes = cipher.doFinal(output, 0, 5, output); + assert numBytes == 5; + } + return ByteBuffer.wrap(output); + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java new file mode 100644 index 00000000000..893eb282116 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.ssl; + +import jdk.internal.net.quic.QuicTLSEngine; +import sun.security.ssl.SSLCipher.SSLWriteCipher; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.LinkedList; + +/** + * {@code OutputRecord} implementation for {@code QuicTLSEngineImpl}. + */ +final class QuicEngineOutputRecord extends OutputRecord implements SSLRecord { + + private final HandshakeFragment fragmenter = new HandshakeFragment(); + + private volatile boolean isCloseWaiting; + + private Alert alert; + + QuicEngineOutputRecord(HandshakeHash handshakeHash) { + super(handshakeHash, SSLWriteCipher.nullTlsWriteCipher()); + + this.packetSize = SSLRecord.maxRecordSize; + this.protocolVersion = ProtocolVersion.NONE; + } + + @Override + public void close() throws IOException { + recordLock.lock(); + try { + if (!isClosed) { + if (!fragmenter.isEmpty()) { + isCloseWaiting = true; + } else { + super.close(); + } + } + } finally { + recordLock.unlock(); + } + } + + boolean isClosed() { + return isClosed || isCloseWaiting; + } + + @Override + void encodeAlert(byte level, byte description) throws IOException { + recordLock.lock(); + try { + if (isClosed()) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("outbound has closed, ignore outbound " + + "alert message: " + Alert.nameOf(description)); + } + return; + } + if (level == Alert.Level.WARNING.level) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Suppressing warning-level " + + "alert message: " + Alert.nameOf(description)); + } + return; + } + + if (alert != null) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Suppressing subsequent alert: " + + description + ", original: " + alert.id); + } + return; + } + + alert = Alert.valueOf(description); + } finally { + recordLock.unlock(); + } + } + + @Override + void encodeHandshake(byte[] source, + int offset, int length) throws IOException { + recordLock.lock(); + try { + if (isClosed()) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("outbound has closed, ignore outbound " + + "handshake message", + ByteBuffer.wrap(source, offset, length)); + } + return; + } + + firstMessage = false; + + byte handshakeType = source[offset]; + if (handshakeHash.isHashable(handshakeType)) { + handshakeHash.deliver(source, offset, length); + } + + fragmenter.queueUpFragment(source, offset, length); + } finally { + recordLock.unlock(); + } + } + + @Override + void encodeChangeCipherSpec() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + void changeWriteCiphers(SSLWriteCipher writeCipher, boolean useChangeCipherSpec) throws IOException { + recordLock.lock(); + try { + fragmenter.changePacketSpace(); + } finally { + recordLock.unlock(); + } + } + + @Override + void changeWriteCiphers(SSLWriteCipher writeCipher, byte keyUpdateRequest) throws IOException { + throw new UnsupportedOperationException("Should not call this"); + } + + @Override + byte[] getHandshakeMessage() { + recordLock.lock(); + try { + return fragmenter.acquireCiphertext(); + } finally { + recordLock.unlock(); + } + } + + @Override + QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() { + recordLock.lock(); + try { + return switch (fragmenter.currentPacketSpace) { + case 0-> QuicTLSEngine.KeySpace.INITIAL; + case 1-> QuicTLSEngine.KeySpace.HANDSHAKE; + case 2-> QuicTLSEngine.KeySpace.ONE_RTT; + default -> throw new IllegalStateException("Unexpected state"); + }; + } finally { + recordLock.unlock(); + } + } + + @Override + boolean isEmpty() { + recordLock.lock(); + try { + return fragmenter.isEmpty(); + } finally { + recordLock.unlock(); + } + } + + Alert getAlert() { + recordLock.lock(); + try { + return alert; + } finally { + recordLock.unlock(); + } + } + + // buffered record fragment + private static class HandshakeMemo { + boolean changeSpace; + byte[] fragment; + } + + static final class HandshakeFragment { + private final LinkedList handshakeMemos = + new LinkedList<>(); + + private int currentPacketSpace; + + void queueUpFragment(byte[] source, + int offset, int length) throws IOException { + HandshakeMemo memo = new HandshakeMemo(); + + memo.fragment = new byte[length]; + assert Record.getInt24(ByteBuffer.wrap(source, offset + 1, 3)) + == length - 4 : "Invalid handshake message length"; + System.arraycopy(source, offset, memo.fragment, 0, length); + + handshakeMemos.add(memo); + } + + void changePacketSpace() { + HandshakeMemo lastMemo = handshakeMemos.peekLast(); + if (lastMemo != null) { + lastMemo.changeSpace = true; + } else { + currentPacketSpace++; + } + } + + byte[] acquireCiphertext() { + HandshakeMemo hsMemo = handshakeMemos.pollFirst(); + if (hsMemo == null) { + return null; + } + if (hsMemo.changeSpace) { + currentPacketSpace++; + } + return hsMemo.fragment; + } + + boolean isEmpty() { + return handshakeMemos.isEmpty(); + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java new file mode 100644 index 00000000000..fb9077af022 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java @@ -0,0 +1,1216 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.IntFunction; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.KDF; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.vm.annotation.Stable; +import sun.security.ssl.QuicCipher.QuicReadCipher; +import sun.security.ssl.QuicCipher.QuicWriteCipher; +import sun.security.util.KeyUtil; + +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.HANDSHAKE; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.INITIAL; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT; +import static jdk.internal.net.quic.QuicTransportErrors.AEAD_LIMIT_REACHED; +import static jdk.internal.net.quic.QuicTransportErrors.KEY_UPDATE_ERROR; +import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR; + +sealed abstract class QuicKeyManager + permits QuicKeyManager.HandshakeKeyManager, + QuicKeyManager.InitialKeyManager, QuicKeyManager.OneRttKeyManager { + + private record QuicKeys(SecretKey key, byte[] iv, SecretKey hp) { + } + + private record CipherPair(QuicReadCipher readCipher, + QuicWriteCipher writeCipher) { + void discard(boolean destroyHP) { + writeCipher.discard(destroyHP); + readCipher.discard(destroyHP); + } + + /** + * {@return true if the keys represented by this {@code CipherPair} + * were used by both this endpoint and the peer, thus implying these + * keys are available to both of them} + */ + boolean usedByBothEndpoints() { + return this.readCipher.hasDecryptedAny() && + this.writeCipher.hasEncryptedAny(); + } + } + + final QuicTLSEngine.KeySpace keySpace; + // counter towards the integrity limit + final AtomicLong invalidPackets = new AtomicLong(); + volatile boolean keysDiscarded; + + private QuicKeyManager(final QuicTLSEngine.KeySpace keySpace) { + this.keySpace = keySpace; + } + + protected abstract boolean keysAvailable(); + + protected abstract QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException; + + protected abstract QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException; + + abstract void discardKeys(); + + void decryptPacket(final long packetNumber, final int keyPhase, + final ByteBuffer packet,final int headerLength, + final ByteBuffer output) throws QuicKeyUnavailableException, + IllegalArgumentException, AEADBadTagException, + QuicTransportException, ShortBufferException { + // keyPhase is only applicable for 1-RTT packets; the decryptPacket + // method is overridden by OneRttKeyManager, so this check is for + // other packet types + if (keyPhase != -1) { + throw new IllegalArgumentException( + "Unexpected key phase value: " + keyPhase); + } + // use current keys to decrypt + QuicReadCipher readCipher = getReadCipher(); + try { + readCipher.decryptPacket(packetNumber, packet, headerLength, output); + } catch (AEADBadTagException e) { + if (invalidPackets.incrementAndGet() >= + readCipher.integrityLimit()) { + throw new QuicTransportException("Integrity limit reached", + keySpace, 0, AEAD_LIMIT_REACHED); + } + throw e; + } + } + + void encryptPacket(final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + // generate the packet header passing the generator the key phase + final ByteBuffer header = headerGenerator.apply(0); // key phase is always 0 for non-ONE_RTT + getWriteCipher().encryptPacket(packetNumber, header, packetPayload, output); + } + + private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion, + final CipherSuite cs, final SecretKey traffic_secret) + throws IOException { + final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs, + traffic_secret); + final QuicTLSData tlsData = getQuicData(quicVersion); + final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel()); + final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel()); + final SecretKey quic_hp = kd.deriveKey(tlsData.getTlsHpLabel()); + return new QuicKeys(quic_key, quic_iv, quic_hp); + } + + // Used in 1RTT when advancing the keyphase. quic_hp is not advanced. + private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion, + final CipherSuite cs, final SecretKey traffic_secret, + final SecretKey quic_hp) throws IOException { + final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs, + traffic_secret); + final QuicTLSData tlsData = getQuicData(quicVersion); + final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel()); + final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel()); + return new QuicKeys(quic_key, quic_iv, quic_hp); + } + + private static QuicTLSData getQuicData(final QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> QuicTLSData.V1; + case QUIC_V2 -> QuicTLSData.V2; + }; + } + + private static byte[] createHkdfInfo(final String label, final int length) { + final byte[] tls13Label = + ("tls13 " + label).getBytes(StandardCharsets.UTF_8); + return createHkdfInfo(tls13Label, length); + } + + private static byte[] createHkdfInfo(final byte[] tls13Label, + final int length) { + final byte[] info = new byte[4 + tls13Label.length]; + final ByteBuffer m = ByteBuffer.wrap(info); + try { + Record.putInt16(m, length); + Record.putBytes8(m, tls13Label); + Record.putInt8(m, 0x00); // zero-length context + } catch (IOException ioe) { + // unlikely + throw new UncheckedIOException("Unexpected exception", ioe); + } + return info; + } + + static final class InitialKeyManager extends QuicKeyManager { + + private volatile CipherPair cipherPair; + + InitialKeyManager() { + super(INITIAL); + } + + @Override + protected boolean keysAvailable() { + return this.cipherPair != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.writeCipher; + } + + @Override + void discardKeys() { + final CipherPair toDiscard = this.cipherPair; + this.keysDiscarded = true; + this.cipherPair = null; // no longer needed + if (toDiscard == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding keys (keyphase=" + + toDiscard.writeCipher.getKeyPhase() + + ") of " + this.keySpace + " key space"); + } + toDiscard.discard(true); + } + + void deriveKeys(final QuicVersion quicVersion, + final byte[] connectionId, + final boolean clientMode) throws IOException{ + Objects.requireNonNull(quicVersion); + final CipherSuite cs = CipherSuite.TLS_AES_128_GCM_SHA256; + final CipherSuite.HashAlg hashAlg = cs.hashAlg; + + KDF hkdf; + try { + hkdf = KDF.getInstance(hashAlg.hkdfAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new SSLHandshakeException("Could not generate secret", e); + } + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + SecretKey initial_secret = null; + SecretKey server_initial_secret = null; + SecretKey client_initial_secret = null; + try { + initial_secret = hkdf.deriveKey("TlsInitialSecret", + HKDFParameterSpec.ofExtract() + .addSalt(tlsData.getInitialSalt()) + .addIKM(connectionId).extractOnly()); + + byte[] clientInfo = createHkdfInfo("client in", + hashAlg.hashLength); + client_initial_secret = + hkdf.deriveKey("TlsClientInitialTrafficSecret", + HKDFParameterSpec.expandOnly( + initial_secret, + clientInfo, + hashAlg.hashLength)); + QuicKeys clientKeys = deriveQuicKeys(quicVersion, cs, + client_initial_secret); + + byte[] serverInfo = createHkdfInfo("server in", + hashAlg.hashLength); + server_initial_secret = + hkdf.deriveKey("TlsServerInitialTrafficSecret", + HKDFParameterSpec.expandOnly( + initial_secret, + serverInfo, + hashAlg.hashLength)); + QuicKeys serverKeys = deriveQuicKeys(quicVersion, cs, + server_initial_secret); + + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + final int keyPhase = 0; + if (clientMode) { + readCipher = QuicCipher.createReadCipher(cs, + server_initial_secret, + serverKeys.key, serverKeys.iv, serverKeys.hp, + keyPhase); + writeCipher = QuicCipher.createWriteCipher(cs, + client_initial_secret, + clientKeys.key, clientKeys.iv, clientKeys.hp, + keyPhase); + } else { + readCipher = QuicCipher.createReadCipher(cs, + client_initial_secret, + clientKeys.key, clientKeys.iv, clientKeys.hp, + keyPhase); + writeCipher = QuicCipher.createWriteCipher(cs, + server_initial_secret, + serverKeys.key, serverKeys.iv, serverKeys.hp, + keyPhase); + } + final CipherPair old = this.cipherPair; + // we don't check if keys are already available, since it's a + // valid case where the INITIAL keys are regenerated due to a + // RETRY packet from the peer or even for the case where a + // different quic version was negotiated by the server + this.cipherPair = new CipherPair(readCipher, writeCipher); + if (old != null) { + old.discard(true); + } + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } finally { + KeyUtil.destroySecretKeys(initial_secret, client_initial_secret, + server_initial_secret); + } + } + + static Cipher getRetryCipher(final QuicVersion quicVersion, + final boolean incoming) throws QuicTransportException { + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + return tlsData.getRetryCipher(incoming); + } + } + + static final class HandshakeKeyManager extends QuicKeyManager { + private volatile CipherPair cipherPair; + + HandshakeKeyManager() { + super(HANDSHAKE); + } + + @Override + protected boolean keysAvailable() { + return this.cipherPair != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.writeCipher; + } + + @Override + void discardKeys() { + final CipherPair toDiscard = this.cipherPair; + this.cipherPair = null; // no longer needed + this.keysDiscarded = true; + if (toDiscard == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding keys (keyphase=" + + toDiscard.writeCipher.getKeyPhase() + + ") of " + this.keySpace + " key space"); + } + toDiscard.discard(true); + } + + void deriveKeys(final QuicVersion quicVersion, + final HandshakeContext handshakeContext, + final boolean clientMode) throws IOException { + Objects.requireNonNull(quicVersion); + if (keysAvailable()) { + throw new IllegalStateException( + "Keys already derived for " + this.keySpace + + " key space"); + } + SecretKey client_handshake_traffic_secret = null; + SecretKey server_handshake_traffic_secret = null; + try { + final SSLKeyDerivation kd = + handshakeContext.handshakeKeyDerivation; + client_handshake_traffic_secret = kd.deriveKey( + "TlsClientHandshakeTrafficSecret"); + final QuicKeys clientKeys = deriveQuicKeys(quicVersion, + handshakeContext.negotiatedCipherSuite, + client_handshake_traffic_secret); + server_handshake_traffic_secret = kd.deriveKey( + "TlsServerHandshakeTrafficSecret"); + final QuicKeys serverKeys = deriveQuicKeys(quicVersion, + handshakeContext.negotiatedCipherSuite, + server_handshake_traffic_secret); + + final CipherSuite negotiatedCipherSuite = + handshakeContext.negotiatedCipherSuite; + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + final int keyPhase = 0; + if (clientMode) { + readCipher = + QuicCipher.createReadCipher(negotiatedCipherSuite, + server_handshake_traffic_secret, + serverKeys.key, serverKeys.iv, + serverKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + client_handshake_traffic_secret, + clientKeys.key, clientKeys.iv, + clientKeys.hp, keyPhase); + } else { + readCipher = + QuicCipher.createReadCipher(negotiatedCipherSuite, + client_handshake_traffic_secret, + clientKeys.key, clientKeys.iv, + clientKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + server_handshake_traffic_secret, + serverKeys.key, serverKeys.iv, + serverKeys.hp, keyPhase); + } + synchronized (this) { + if (this.cipherPair != null) { + // don't allow setting more than once + throw new IllegalStateException("Keys already " + + "available for keyspace: " + + this.keySpace); + } + this.cipherPair = new CipherPair(readCipher, writeCipher); + } + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } finally { + KeyUtil.destroySecretKeys(client_handshake_traffic_secret, + server_handshake_traffic_secret); + } + } + } + + static final class OneRttKeyManager extends QuicKeyManager { + // a series of keys that the 1-RTT key manager uses + private record KeySeries(QuicReadCipher old, CipherPair current, + CipherPair next) { + private KeySeries { + Objects.requireNonNull(current); + if (old != null) { + if (old.getKeyPhase() == + current.writeCipher.getKeyPhase()) { + throw new IllegalArgumentException("Both old keys and" + + " current keys have the same key phase: " + + current.writeCipher.getKeyPhase()); + } + } + if (next != null) { + if (next.writeCipher.getKeyPhase() == + current.writeCipher.getKeyPhase()) { + throw new IllegalArgumentException("Both next keys " + + "and current keys have the same key phase: " + + current.writeCipher.getKeyPhase()); + } + } + } + + /** + * {@return true if this {@code KeySeries} has an old decryption key + * and the {@code pktNum} is lower than the least packet number the + * current decryption key has decrypted so far} + * + * @param pktNum the packet number for which the old key + * might be needed + */ + boolean canUseOldDecryptKey(final long pktNum) { + assert pktNum >= 0 : "unexpected packet number: " + pktNum; + if (this.old == null) { + return false; + } + final QuicReadCipher currentKey = this.current.readCipher; + final long lowestDecrypted = currentKey.lowestDecryptedPktNum(); + // if the incoming packet number is lesser than the lowest + // decrypted packet number by the current key, then it + // implies that this might be a delayed packet and thus is + // allowed to use the old key (if available) from + // the previous key phase. + // see RFC-9001, section 6.5 + if (lowestDecrypted == -1) { + return true; + } + return pktNum < lowestDecrypted; + } + } + + // will be set when the keys are derived + private volatile QuicVersion negotiatedVersion; + + private final Lock keySeriesLock = new ReentrantLock(); + // will be set when keys are derived and will + // be updated whenever keys are updated. + // Must be updated/written only + // when holding the keySeriesLock lock + private volatile KeySeries keySeries; + + @Stable + private volatile QuicOneRttContext oneRttContext; + + OneRttKeyManager() { + super(ONE_RTT); + } + + @Override + protected boolean keysAvailable() { + return this.keySeries != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final KeySeries series = requireKeySeries(); + return series.current.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final KeySeries series = requireKeySeries(); + return series.current.writeCipher; + } + + @Override + void discardKeys() { + this.keysDiscarded = true; + final KeySeries series; + this.keySeriesLock.lock(); + try { + series = this.keySeries; + this.keySeries = null; // no longer available + } finally { + this.keySeriesLock.unlock(); + } + if (series == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding key (series) of " + + this.keySpace + " key space"); + } + if (series.old != null) { + series.old.discard(false); + } + discardKeys(series.current); + discardKeys(series.next); + } + + @Override + void decryptPacket(final long packetNumber, final int keyPhase, + final ByteBuffer packet, final int headerLength, + final ByteBuffer output) throws QuicKeyUnavailableException, + QuicTransportException, AEADBadTagException, ShortBufferException { + if (keyPhase != 0 && keyPhase != 1) { + throw new IllegalArgumentException("Unexpected key phase " + + "value: " + keyPhase); + } + final KeySeries series = requireKeySeries(); + final CipherPair current = series.current; + // Use the write cipher's key phase to detect a key update as noted + // in RFC-9001, section 6.2: + // An endpoint detects a key update when processing a packet with + // a key phase that differs from the value used to protect the + // last packet it sent. + final int currentKeyPhase = current.writeCipher.getKeyPhase(); + if (keyPhase == currentKeyPhase) { + current.readCipher.decryptPacket(packetNumber, packet, + headerLength, output); + return; + } + // incoming packet is using a key phase which doesn't match the + // current key phase. this implies that either a key update + // is being initiated or a key update initiated by the current + // endpoint is in progress and some older packet with the + // previous key phase has arrived. + if (series.canUseOldDecryptKey(packetNumber)) { + final QuicReadCipher oldReadCipher = series.old; + assert oldReadCipher != null : "old key is unexpectedly null"; + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("using old read key to decrypt packet: " + + packetNumber + ", with incoming key phase: " + + keyPhase + ", current key phase: " + + currentKeyPhase); + } + oldReadCipher.decryptPacket( + packetNumber, packet, headerLength, output); + // we were able to decrypt using an old key. now verify + // that it was OK to use this old key for this packet. + if (!series.current.usedByBothEndpoints() + && series.current.writeCipher.hasEncryptedAny() + && oneRttContext.getLargestPeerAckedPN() + >= series.current.writeCipher.lowestEncryptedPktNum()) { + // RFC-9001, section 6.2: + // An endpoint that receives an acknowledgment that is + // carried in a packet protected with old keys where any + // acknowledged packet was protected with newer keys MAY + // treat that as a connection error of type + // KEY_UPDATE_ERROR. This indicates that a peer has + // received and acknowledged a packet that initiates a key + // update, but has not updated keys in response. + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("peer used incorrect key, was" + + " expected to use updated key of" + + " key phase: " + currentKeyPhase + + ", incoming key phase: " + keyPhase + + ", packet number: " + packetNumber); + } + throw new QuicTransportException("peer used incorrect" + + " key, was expected to use updated key", + this.keySpace, 0, KEY_UPDATE_ERROR); + } + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("detected ONE_RTT key update, current key " + + "phase: " + currentKeyPhase + + ", incoming key phase: " + keyPhase + + ", packet number: " + packetNumber); + } + decryptUsingNextKeys( + series, packetNumber, packet, headerLength, output); + } + + @Override + void encryptPacket(final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + KeySeries currentSeries = requireKeySeries(); + if (currentSeries.next == null) { + // next keys haven't yet been generated, + // generate them now + try { + currentSeries = generateNextKeys( + this.negotiatedVersion, currentSeries); + } catch (GeneralSecurityException | IOException e) { + throw new QuicTransportException("Failed to update keys", + ONE_RTT, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + maybeInitiateKeyUpdate(currentSeries, packetNumber); + // call getWriteCipher() afresh so that it can use + // the new keyseries if at all the key update was + // initiated + final QuicWriteCipher writeCipher = getWriteCipher(); + final int keyPhase = writeCipher.getKeyPhase(); + // generate the packet header passing the generator the key phase + final ByteBuffer header = headerGenerator.apply(keyPhase); + writeCipher.encryptPacket(packetNumber, header, packetPayload, output); + } + + void setOneRttContext(final QuicOneRttContext ctx) { + Objects.requireNonNull(ctx); + this.oneRttContext = ctx; + } + + private KeySeries requireKeySeries() + throws QuicKeyUnavailableException { + final KeySeries series = this.keySeries; + if (series != null) { + return series; + } + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + + // based on certain internal criteria, this method may trigger a key + // update. + // returns true if it does trigger the key update. false otherwise. + private boolean maybeInitiateKeyUpdate(final KeySeries currentSeries, + final long packetNumber) { + final QuicWriteCipher cipher = currentSeries.current.writeCipher; + // when we notice that we have reached 80% (which is arbitrary) + // of the confidentiality limit, we trigger a key update instead + // of waiting to hit the limit + final long confidentialityLimit = cipher.confidentialityLimit(); + if (confidentialityLimit < 0) { + return false; + } + final long numEncrypted = cipher.getNumEncrypted(); + if (numEncrypted >= 0.8 * confidentialityLimit) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("about to reach confidentiality limit, " + + "attempting to initiate a 1-RTT key update," + + " packet number: " + + packetNumber + ", current key phase: " + + cipher.getKeyPhase()); + } + final boolean initiated = initiateKeyUpdate(currentSeries); + if (initiated) { + final int newKeyPhase = + this.keySeries.current.writeCipher.getKeyPhase(); + assert cipher.getKeyPhase() != newKeyPhase + : "key phase of updated key unexpectedly matches " + + "the key phase " + + cipher.getKeyPhase() + " of current keys"; + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "1-RTT key update initiated, new key phase: " + + newKeyPhase); + } + } + return initiated; + } + return false; + } + + private boolean initiateKeyUpdate(final KeySeries series) { + // we only initiate a key update if this current endpoint and the + // peer have both been using this current key + if (!series.current.usedByBothEndpoints()) { + // RFC-9001, section 6.1: + // An endpoint MUST NOT initiate a subsequent key update + // unless it has received + // an acknowledgment for a packet that was sent protected + // with keys from the + // current key phase. This ensures that keys are + // available to both peers before + // another key update can be initiated. + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "skipping key update initiation because peer " + + "hasn't yet sent us a packet encrypted with " + + "current key of key phase: " + + series.current.readCipher.getKeyPhase()); + } + return false; + } + // OK to initiate a key update. + // An endpoint initiates a key update by updating its packet + // protection write secret + // and using that to protect new packets. + rolloverKeys(this.negotiatedVersion, series); + return true; + } + + private static void discardKeys(final CipherPair cipherPair) { + if (cipherPair == null) { + return; + } + cipherPair.discard(true); + } + + /** + * uses "next" keys to try and decrypt the incoming packet. if that + * succeeded then it implies that the key update was indeed initiated by + * the peer and this method then rolls over the keys to start using + * these "next" keys. this method then returns true in such cases. if + * the packet decryption using the "next" key fails, then this method + * just returns back false (and doesn't roll over the keys) + */ + private void decryptUsingNextKeys( + final KeySeries currentKeySeries, + final long packetNumber, + final ByteBuffer packet, + final int headerLength, + final ByteBuffer output) + throws QuicKeyUnavailableException, AEADBadTagException, + ShortBufferException, QuicTransportException { + if (currentKeySeries.next == null) { + // this can happen if the peer initiated another + // key update before we could generate the next + // keys during our encryption flow. in such + // cases we reject the key update for the packet + // (we avoid timing attacks by not generating + // keys during decryption, our key generation + // only happens during encryption) + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("next keys unavailable," + + " won't decrypt a packet which appears to be" + + " a key update"); + } + throw new QuicKeyUnavailableException( + "next keys unavailable to handle key update", + this.keySpace); + } + // use the next keys to attempt decrypting + currentKeySeries.next.readCipher.decryptPacket(packetNumber, packet, + headerLength, output); + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "decrypted using next keys for peer-initiated" + + " key update; will now switch to new key phase: " + + currentKeySeries.next.readCipher.getKeyPhase()); + } + // we have successfully decrypted the packet using the new/next + // read key. So we now update even the write key as noted in + // RFC-9001, section 6.2: + // If a packet is successfully processed using the next key and + // IV, then the peer has initiated a key update. The endpoint + // MUST update its send keys to the corresponding + // key phase in response, as described in Section 6.1. Sending + // keys MUST be updated before sending an acknowledgment for the + // packet that was received with updated keys. rollover the + // keys == old gets discarded and is replaced by + // current, current is replaced by next and next is set to null + // (a new set of next keys will be generated separately on + // a schedule) + rolloverKeys(this.negotiatedVersion, currentKeySeries); + } + + void deriveKeys(final QuicVersion negotiatedVersion, + final HandshakeContext handshakeContext, + final boolean clientMode) throws IOException { + Objects.requireNonNull(negotiatedVersion); + if (keysAvailable()) { + throw new IllegalStateException("Keys already derived for " + + this.keySpace + " key space"); + } + this.negotiatedVersion = negotiatedVersion; + + try { + SSLKeyDerivation kd = handshakeContext.handshakeKeyDerivation; + SecretKey client_application_traffic_secret_0 = kd.deriveKey( + "TlsClientAppTrafficSecret"); + SecretKey server_application_traffic_secret_0 = kd.deriveKey( + "TlsServerAppTrafficSecret"); + + deriveOneRttKeys(this.negotiatedVersion, + client_application_traffic_secret_0, + server_application_traffic_secret_0, + handshakeContext.negotiatedCipherSuite, + clientMode); + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } + } + + void deriveOneRttKeys(final QuicVersion version, + final SecretKey client_application_traffic_secret_0, + final SecretKey server_application_traffic_secret_0, + final CipherSuite negotiatedCipherSuite, + final boolean clientMode) throws IOException, + GeneralSecurityException { + final QuicKeys clientKeys = deriveQuicKeys(version, + negotiatedCipherSuite, + client_application_traffic_secret_0); + final QuicKeys serverKeys = deriveQuicKeys(version, + negotiatedCipherSuite, + server_application_traffic_secret_0); + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + // this method always derives the first key for the 1-RTT, so key + // phase is always 0 + final int keyPhase = 0; + if (clientMode) { + readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite, + server_application_traffic_secret_0, serverKeys.key, + serverKeys.iv, serverKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + client_application_traffic_secret_0, clientKeys.key, + clientKeys.iv, clientKeys.hp, keyPhase); + } else { + readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite, + client_application_traffic_secret_0, clientKeys.key, + clientKeys.iv, clientKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + server_application_traffic_secret_0, serverKeys.key, + serverKeys.iv, serverKeys.hp, keyPhase); + } + // generate the next set of keys beforehand to prevent any timing + // attacks + // during key update + final QuicReadCipher nPlus1ReadCipher = + generateNextReadCipher(version, readCipher); + final QuicWriteCipher nPlus1WriteCipher = + generateNextWriteCipher(version, writeCipher); + this.keySeriesLock.lock(); + try { + if (this.keySeries != null) { + // don't allow deriving the first set of 1-RTT keys more + // than once + throw new IllegalStateException("Keys already available " + + "for keyspace: " + + this.keySpace); + } + this.keySeries = new KeySeries(null, + new CipherPair(readCipher, writeCipher), + new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher)); + } finally { + this.keySeriesLock.unlock(); + } + } + + private static QuicWriteCipher generateNextWriteCipher( + final QuicVersion quicVersion, final QuicWriteCipher current) + throws IOException, GeneralSecurityException { + final SSLKeyDerivation kd = + new QuicTLSKeyDerivation(current.getCipherSuite(), + current.getBaseSecret()); + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + final SecretKey nplus1Secret = + kd.deriveKey(tlsData.getTlsKeyUpdateLabel()); + final QuicKeys quicKeys = + QuicKeyManager.deriveQuicKeys(quicVersion, + current.getCipherSuite(), + nplus1Secret, current.getHeaderProtectionKey()); + final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0; + // toggle the 1 bit keyphase + final QuicWriteCipher next = + QuicCipher.createWriteCipher(current.getCipherSuite(), + nplus1Secret, quicKeys.key, quicKeys.iv, + quicKeys.hp, nextKeyPhase); + return next; + } + + private static QuicReadCipher generateNextReadCipher( + final QuicVersion quicVersion, final QuicReadCipher current) + throws IOException, GeneralSecurityException { + final SSLKeyDerivation kd = + new QuicTLSKeyDerivation(current.getCipherSuite(), + current.getBaseSecret()); + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + final SecretKey nPlus1Secret = + kd.deriveKey(tlsData.getTlsKeyUpdateLabel()); + final QuicKeys quicKeys = + QuicKeyManager.deriveQuicKeys(quicVersion, + current.getCipherSuite(), + nPlus1Secret, current.getHeaderProtectionKey()); + final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0; + // toggle the 1 bit keyphase + final QuicReadCipher next = + QuicCipher.createReadCipher(current.getCipherSuite(), + nPlus1Secret, quicKeys.key, + quicKeys.iv, quicKeys.hp, nextKeyPhase); + return next; + } + + private KeySeries generateNextKeys(final QuicVersion version, + final KeySeries currentSeries) + throws GeneralSecurityException, IOException { + this.keySeriesLock.lock(); + try { + // nothing to do if some other thread + // already changed the keySeries + if (this.keySeries != currentSeries) { + return this.keySeries; + } + final QuicReadCipher nPlus1ReadCipher = + generateNextReadCipher(version, + currentSeries.current.readCipher); + final QuicWriteCipher nPlus1WriteCipher = + generateNextWriteCipher(version, + currentSeries.current.writeCipher); + // only the next keys will differ in the new series + // as compared to the current series + final KeySeries newSeries = new KeySeries(currentSeries.old, + currentSeries.current, + new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher)); + this.keySeries = newSeries; + return newSeries; + } finally { + this.keySeriesLock.unlock(); + } + } + + /** + * Updates the key series by "left shifting" the series of keys. + * i.e. old keys (if any) are discarded, current keys + * are moved to old keys and next keys are moved to current keys. + * Note that no new keys will be generated by this method. + * @return the key series that will be in use going forward + */ + private KeySeries rolloverKeys(final QuicVersion version, + final KeySeries currentSeries) { + this.keySeriesLock.lock(); + try { + // nothing to do if some other thread + // already changed the keySeries + if (this.keySeries != currentSeries) { + return this.keySeries; + } + assert currentSeries.next != null : "Key series missing next" + + " keys"; + // discard the old read cipher which will no longer be used + final QuicReadCipher oldReadCipher = currentSeries.old; + // once we move current key to old, we won't be using the + // write cipher of that + // moved pair + final QuicWriteCipher writeCipherToDiscard = + currentSeries.current.writeCipher; + final KeySeries newSeries = new KeySeries( + currentSeries.current.readCipher, currentSeries.next, + null); + // update the key series + this.keySeries = newSeries; + if (oldReadCipher != null) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "discarding old read key of key phase: " + + oldReadCipher.getKeyPhase()); + } + oldReadCipher.discard(false); + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding write key of key phase: " + + writeCipherToDiscard.getKeyPhase()); + } + writeCipherToDiscard.discard(false); + return newSeries; + } finally { + this.keySeriesLock.unlock(); + } + } + } + + private static final class QuicTLSKeyDerivation + implements SSLKeyDerivation { + + private enum HkdfLabel { + // RFC 9001: quic version 1 + quickey("quic key"), + quiciv("quic iv"), + quichp("quic hp"), + quicku("quic ku"), + + // RFC 9369: quic version 2 + quicv2key("quicv2 key"), + quicv2iv("quicv2 iv"), + quicv2hp("quicv2 hp"), + quicv2ku("quicv2 ku"); + + private final String label; + private final byte[] tls13LabelBytes; + + HkdfLabel(final String label) { + Objects.requireNonNull(label); + this.label = label; + this.tls13LabelBytes = + ("tls13 " + label).getBytes(StandardCharsets.UTF_8); + } + + private static HkdfLabel fromLabel(final String label) { + Objects.requireNonNull(label); + for (final HkdfLabel hkdfLabel : HkdfLabel.values()) { + if (hkdfLabel.label.equals(label)) { + return hkdfLabel; + } + } + throw new IllegalArgumentException( + "unrecognized label: " + label); + } + } + + private final CipherSuite cs; + private final SecretKey secret; + + private QuicTLSKeyDerivation(final CipherSuite cs, + final SecretKey secret) { + this.cs = Objects.requireNonNull(cs); + this.secret = Objects.requireNonNull(secret); + } + + @Override + public SecretKey deriveKey(final String algorithm) throws IOException { + final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm); + try { + final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm); + final int keyLength = getKeyLength(hkdfLabel); + final byte[] hkdfInfo = + createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength); + final String keyAlgo = getKeyAlgorithm(hkdfLabel); + return hkdf.deriveKey(keyAlgo, + HKDFParameterSpec.expandOnly( + secret, hkdfInfo, keyLength)); + } catch (GeneralSecurityException gse) { + throw new SSLHandshakeException("Could not derive key", gse); + } + } + + @Override + public byte[] deriveData(final String algorithm) throws IOException { + final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm); + try { + final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm); + final int keyLength = getKeyLength(hkdfLabel); + final byte[] hkdfInfo = + createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength); + return hkdf.deriveData(HKDFParameterSpec.expandOnly( + secret, hkdfInfo, keyLength)); + } catch (GeneralSecurityException gse) { + throw new SSLHandshakeException("Could not derive key", gse); + } + } + + private int getKeyLength(final HkdfLabel hkdfLabel) { + return switch (hkdfLabel) { + case quicku, quicv2ku -> { + // RFC-9001, section 6.1: + // secret_ = HKDF-Expand-Label(secret_, "quic + // ku", "", Hash.length) + yield this.cs.hashAlg.hashLength; + } + case quiciv, quicv2iv -> this.cs.bulkCipher.ivSize; + default -> this.cs.bulkCipher.keySize; + }; + } + + private String getKeyAlgorithm(final HkdfLabel hkdfLabel) { + return switch (hkdfLabel) { + case quicku, quicv2ku -> "TlsUpdateNplus1"; + case quiciv, quicv2iv -> + throw new IllegalArgumentException("IV not expected"); + default -> this.cs.bulkCipher.algorithm; + }; + } + } + + private enum QuicTLSData { + V1("38762cf7f55934b34d179ae6a4c80cadccbb7f0a", + "be0c690b9f66575a1d766b54e368c84e", + "461599d35d632bf2239825bb", + "quic key", "quic iv", "quic hp", "quic ku"), + V2("0dede3def700a6db819381be6e269dcbf9bd2ed9", + "8fb4b01b56ac48e260fbcbcead7ccc92", + "d86969bc2d7c6d9990efb04a", + "quicv2 key", "quicv2 iv", "quicv2 hp", "quicv2 ku"); + + private final byte[] initialSalt; + private final SecretKey retryKey; + private final GCMParameterSpec retryIvSpec; + private final String keyLabel; + private final String ivLabel; + private final String hpLabel; + private final String kuLabel; + + QuicTLSData(String initialSalt, String retryKey, String retryIv, + String keyLabel, String ivLabel, String hpLabel, + String kuLabel) { + this.initialSalt = HexFormat.of() + .parseHex(initialSalt); + this.retryKey = new SecretKeySpec(HexFormat.of() + .parseHex(retryKey), "AES"); + retryIvSpec = new GCMParameterSpec(128, + HexFormat.of().parseHex(retryIv)); + this.keyLabel = keyLabel; + this.ivLabel = ivLabel; + this.hpLabel = hpLabel; + this.kuLabel = kuLabel; + } + + public byte[] getInitialSalt() { + return initialSalt; + } + + public Cipher getRetryCipher(boolean incoming) throws QuicTransportException { + Cipher retryCipher = null; + try { + retryCipher = Cipher.getInstance("AES/GCM/NoPadding"); + retryCipher.init(incoming ? Cipher.DECRYPT_MODE : + Cipher.ENCRYPT_MODE, + retryKey, retryIvSpec); + } catch (Exception e) { + throw new QuicTransportException("Cipher not available", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + return retryCipher; + } + + public String getTlsKeyLabel() { + return keyLabel; + } + + public String getTlsIvLabel() { + return ivLabel; + } + + public String getTlsHpLabel() { + return hpLabel; + } + + public String getTlsKeyUpdateLabel() { + return kuLabel; + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java new file mode 100644 index 00000000000..6765f554fcc --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java @@ -0,0 +1,893 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.quic.QuicVersion; +import sun.security.ssl.QuicKeyManager.HandshakeKeyManager; +import sun.security.ssl.QuicKeyManager.InitialKeyManager; +import sun.security.ssl.QuicKeyManager.OneRttKeyManager; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.security.AlgorithmConstraints; +import java.security.GeneralSecurityException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicTLSEngine.HandshakeState.*; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.*; + +/** + * One instance per QUIC connection. Configuration methods similar to + * SSLEngine. + *

    + * The implementation of this class uses the {@link QuicKeyManager} to maintain + * all state relating to keys for each encryption levels. + */ +public final class QuicTLSEngineImpl implements QuicTLSEngine, SSLTransport { + + private static final Map messageTypeMap = + Map.of(SSLHandshake.CLIENT_HELLO.id, INITIAL, + SSLHandshake.SERVER_HELLO.id, INITIAL, + SSLHandshake.ENCRYPTED_EXTENSIONS.id, HANDSHAKE, + SSLHandshake.CERTIFICATE_REQUEST.id, HANDSHAKE, + SSLHandshake.CERTIFICATE.id, HANDSHAKE, + SSLHandshake.CERTIFICATE_VERIFY.id, HANDSHAKE, + SSLHandshake.FINISHED.id, HANDSHAKE, + SSLHandshake.NEW_SESSION_TICKET.id, ONE_RTT); + static final long BASE_CRYPTO_ERROR = 256; + + private static final Set SUPPORTED_QUIC_VERSIONS = + Set.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2); + + // VarHandles are used to access compareAndSet semantics. + private static final VarHandle HANDSHAKE_STATE_HANDLE; + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + final Class quicTlsEngineImpl = QuicTLSEngineImpl.class; + HANDSHAKE_STATE_HANDLE = lookup.findVarHandle( + quicTlsEngineImpl, + "handshakeState", + HandshakeState.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private final TransportContext conContext; + private final String peerHost; + private final int peerPort; + private volatile HandshakeState handshakeState; + private volatile KeySpace sendKeySpace; + // next message to send or receive + private volatile ByteBuffer localQuicTransportParameters; + private volatile QuicTransportParametersConsumer + remoteQuicTransportParametersConsumer; + + // keymanagers for individual keyspaces + private final InitialKeyManager initialKeyManager = new InitialKeyManager(); + private final HandshakeKeyManager handshakeKeyManager = + new HandshakeKeyManager(); + private final OneRttKeyManager oneRttKeyManager = new OneRttKeyManager(); + + // buffer for crypto data that was received but not yet processed (i.e. + // incomplete messages) + private volatile ByteBuffer incomingCryptoBuffer; + // key space for incomingCryptoBuffer + private volatile KeySpace incomingCryptoSpace; + + private volatile QuicVersion negotiatedVersion; + + public QuicTLSEngineImpl(SSLContextImpl sslContextImpl) { + this(sslContextImpl, null, -1); + } + + public QuicTLSEngineImpl(SSLContextImpl sslContextImpl, final String peerHost, final int peerPort) { + this.peerHost = peerHost; + this.peerPort = peerPort; + this.sendKeySpace = INITIAL; + HandshakeHash handshakeHash = new HandshakeHash(); + this.conContext = new TransportContext(sslContextImpl, this, + new SSLEngineInputRecord(handshakeHash), + new QuicEngineOutputRecord(handshakeHash)); + conContext.sslConfig.enabledProtocols = List.of(ProtocolVersion.TLS13); + if (peerHost != null) { + conContext.sslConfig.serverNames = + Utilities.addToSNIServerNameList( + conContext.sslConfig.serverNames, peerHost); + } + conContext.setQuic(true); + } + + @Override + public void setUseClientMode(boolean mode) { + conContext.setUseClientMode(mode); + this.handshakeState = mode + ? HandshakeState.NEED_SEND_CRYPTO + : HandshakeState.NEED_RECV_CRYPTO; + } + + @Override + public boolean getUseClientMode() { + return conContext.sslConfig.isClientMode; + } + + @Override + public void setSSLParameters(final SSLParameters params) { + Objects.requireNonNull(params); + // section, 4.2 of RFC-9001 + // Clients MUST NOT offer TLS versions older than 1.3 + final String[] protos = params.getProtocols(); + if (protos == null || protos.length == 0) { + throw new IllegalArgumentException("No TLS protocols set"); + } + boolean tlsv13Present = false; + Set unsupported = new HashSet<>(); + for (String p : protos) { + if ("TLSv1.3".equals(p)) { + tlsv13Present = true; + } else { + unsupported.add(p); + } + } + if (!tlsv13Present) { + throw new IllegalArgumentException( + "required TLSv1.3 protocol version hasn't been set"); + } + if (!unsupported.isEmpty()) { + throw new IllegalArgumentException( + "Unsupported TLS protocol versions " + unsupported); + } + conContext.sslConfig.setSSLParameters(params); + } + + @Override + public SSLSession getSession() { + return conContext.conSession; + } + + @Override + public SSLSession getHandshakeSession() { + final HandshakeContext handshakeContext = conContext.handshakeContext; + return handshakeContext == null + ? null + : handshakeContext.handshakeSession; + } + + /** + * {@return the {@link AlgorithmConstraints} that are applicable for this engine, + * or null if none are applicable} + */ + AlgorithmConstraints getAlgorithmConstraints() { + final HandshakeContext handshakeContext = conContext.handshakeContext; + // if we are handshaking then use the handshake context + // to determine the constraints, else use the configured + // SSLParameters + return handshakeContext == null + ? getSSLParameters().getAlgorithmConstraints() + : handshakeContext.sslConfig.userSpecifiedAlgorithmConstraints; + } + + @Override + public SSLParameters getSSLParameters() { + return conContext.sslConfig.getSSLParameters(); + } + + @Override + public String getApplicationProtocol() { + // TODO: review thread safety when dealing with conContext + return conContext.applicationProtocol; + } + + @Override + public Set getSupportedQuicVersions() { + return SUPPORTED_QUIC_VERSIONS; + } + + @Override + public void setOneRttContext(final QuicOneRttContext ctx) { + this.oneRttKeyManager.setOneRttContext(ctx); + } + + private QuicVersion getNegotiatedVersion() { + final QuicVersion negotiated = this.negotiatedVersion; + if (negotiated == null) { + throw new IllegalStateException( + "Quic version hasn't been negotiated yet"); + } + return negotiated; + } + + private boolean isEnabled(final QuicVersion quicVersion) { + final Set enabled = getSupportedQuicVersions(); + if (enabled == null) { + return false; + } + return enabled.contains(quicVersion); + } + + /** + * Returns the current handshake state of the connection. Sometimes packets + * that could be decrypted can be received before the handshake has + * completed, but should not be decrypted until it is complete + * + * @return the HandshakeState + */ + @Override + public HandshakeState getHandshakeState() { + return handshakeState; + } + + /** + * Returns the current sending key space (encryption level) + * + * @return the current sending key space + */ + @Override + public KeySpace getCurrentSendKeySpace() { + return sendKeySpace; + } + + @Override + public boolean keysAvailable(KeySpace keySpace) { + return switch (keySpace) { + case INITIAL -> this.initialKeyManager.keysAvailable(); + case HANDSHAKE -> this.handshakeKeyManager.keysAvailable(); + case ONE_RTT -> this.oneRttKeyManager.keysAvailable(); + case ZERO_RTT -> false; + case RETRY -> true; + default -> throw new IllegalArgumentException( + keySpace + " not expected here"); + }; + } + + @Override + public void discardKeys(KeySpace keySpace) { + switch (keySpace) { + case INITIAL -> this.initialKeyManager.discardKeys(); + case HANDSHAKE -> this.handshakeKeyManager.discardKeys(); + case ONE_RTT -> this.oneRttKeyManager.discardKeys(); + default -> throw new IllegalArgumentException( + "key discarding not implemented for " + keySpace); + } + } + + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return switch (keySpace) { + case INITIAL, HANDSHAKE, ZERO_RTT, ONE_RTT -> 16; + default -> throw new IllegalArgumentException( + "Type '" + keySpace + "' not expected here"); + }; + } + + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, + boolean incoming, ByteBuffer sample) + throws QuicKeyUnavailableException, QuicTransportException { + final QuicKeyManager keyManager = keyManager(keySpace); + if (incoming) { + final QuicCipher.QuicReadCipher quicCipher = + keyManager.getReadCipher(); + return quicCipher.computeHeaderProtectionMask(sample); + } else { + final QuicCipher.QuicWriteCipher quicCipher = + keyManager.getWriteCipher(); + return quicCipher.computeHeaderProtectionMask(sample); + } + } + + @Override + public int getAuthTagSize() { + // RFC-9001, section 5.3 + // QUIC can use any of the cipher suites defined in [TLS13] with the + // exception of TLS_AES_128_CCM_8_SHA256. ... + // These cipher suites have a 16-byte authentication tag and produce + // an output 16 bytes larger than their input. + return 16; + } + + @Override + public void encryptPacket(final KeySpace keySpace, final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + final QuicKeyManager keyManager = keyManager(keySpace); + keyManager.encryptPacket(packetNumber, headerGenerator, packetPayload, output); + } + + @Override + public void decryptPacket(final KeySpace keySpace, + final long packetNumber, final int keyPhase, + final ByteBuffer packet, final int headerLength, + final ByteBuffer output) + throws QuicKeyUnavailableException, AEADBadTagException, + QuicTransportException, ShortBufferException { + if (keySpace == ONE_RTT && !isTLSHandshakeComplete()) { + // RFC-9001, section 5.7 specifies that the server or the client MUST NOT + // decrypt 1-RTT packets, even if 1-RTT keys are available, before the + // TLS handshake is complete. + throw new QuicKeyUnavailableException("QUIC TLS handshake not yet complete", ONE_RTT); + } + final QuicKeyManager keyManager = keyManager(keySpace); + keyManager.decryptPacket(packetNumber, keyPhase, packet, headerLength, + output); + } + + @Override + public void signRetryPacket(final QuicVersion quicVersion, + final ByteBuffer originalConnectionId, final ByteBuffer packet, + final ByteBuffer output) + throws ShortBufferException, QuicTransportException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException( + "Quic version " + quicVersion + " isn't enabled"); + } + int connIdLength = originalConnectionId.remaining(); + if (connIdLength >= 256 || connIdLength < 0) { + throw new IllegalArgumentException("connection ID length"); + } + final Cipher cipher = InitialKeyManager.getRetryCipher( + quicVersion, false); + cipher.updateAAD(new byte[]{(byte) connIdLength}); + cipher.updateAAD(originalConnectionId); + cipher.updateAAD(packet); + try { + // No data to encrypt, just outputting the tag which will be + // verified later. + cipher.doFinal(ByteBuffer.allocate(0), output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Failed to sign packet", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + + @Override + public void verifyRetryPacket(final QuicVersion quicVersion, + final ByteBuffer originalConnectionId, + final ByteBuffer packet) + throws AEADBadTagException, QuicTransportException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException( + "Quic version " + quicVersion + " isn't enabled"); + } + int connIdLength = originalConnectionId.remaining(); + if (connIdLength >= 256 || connIdLength < 0) { + throw new IllegalArgumentException("connection ID length"); + } + int originalLimit = packet.limit(); + packet.limit(originalLimit - 16); + final Cipher cipher = + InitialKeyManager.getRetryCipher(quicVersion, true); + cipher.updateAAD(new byte[]{(byte) connIdLength}); + cipher.updateAAD(originalConnectionId); + cipher.updateAAD(packet); + packet.limit(originalLimit); + try { + assert packet.remaining() == 16; + int outBufLength = cipher.getOutputSize(packet.remaining()); + // No data to decrypt, just checking the tag. + ByteBuffer outBuffer = ByteBuffer.allocate(outBufLength); + cipher.doFinal(packet, outBuffer); + assert outBuffer.position() == 0; + } catch (AEADBadTagException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Failed to verify packet", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + + private QuicKeyManager keyManager(final KeySpace keySpace) { + return switch (keySpace) { + case INITIAL -> this.initialKeyManager; + case HANDSHAKE -> this.handshakeKeyManager; + case ONE_RTT -> this.oneRttKeyManager; + default -> throw new IllegalArgumentException( + "No key manager available for key space: " + keySpace); + }; + } + + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException { + if (keySpace != sendKeySpace) { + throw new IllegalStateException("Unexpected key space: " + + keySpace + " (expected " + sendKeySpace + ")"); + } + if (handshakeState == HandshakeState.NEED_SEND_CRYPTO || + !conContext.outputRecord.isEmpty()) { // session ticket + byte[] bytes = produceNextHandshakeMessage(); + return ByteBuffer.wrap(bytes); + } else { + return null; + } + } + + private byte[] produceNextHandshakeMessage() throws IOException { + if (!conContext.isNegotiated && !conContext.isBroken && + !conContext.isInboundClosed() && + !conContext.isOutboundClosed()) { + conContext.kickstart(); + } + byte[] message = conContext.outputRecord.getHandshakeMessage(); + if (handshakeState == NEED_SEND_CRYPTO) { + if (conContext.outputRecord.isEmpty()) { + if (conContext.isNegotiated) { + // client, done + handshakeState = NEED_RECV_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + handshakeState = NEED_RECV_CRYPTO; + } + } else if (sendKeySpace == INITIAL && !getUseClientMode()) { + // Server sends handshake messages immediately after + // the initial server hello. Need to check the next key space. + sendKeySpace = conContext.outputRecord.getHandshakeMessageKeySpace(); + } + } else { + assert conContext.isNegotiated; + } + return message; + } + + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) + throws QuicTransportException { + if (!payload.hasRemaining()) { + throw new IllegalArgumentException("Empty crypto buffer"); + } + if (keySpace == KeySpace.ZERO_RTT) { + throw new IllegalArgumentException("Crypto in zero-rtt"); + } + if (incomingCryptoSpace != null && incomingCryptoSpace != keySpace) { + throw new QuicTransportException("Unexpected message", null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException( + "Unfinished message in " + incomingCryptoSpace)); + } + try { + if (!conContext.isNegotiated && !conContext.isBroken && + !conContext.isInboundClosed() && + !conContext.isOutboundClosed()) { + conContext.kickstart(); + } + } catch (IOException e) { + throw new QuicTransportException(e.toString(), null, 0, + BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + // previously unconsumed bytes in incomingCryptoBuffer, new bytes in + // payload. if incomingCryptoBuffer is not null, it's either 4 bytes + // or large enough to hold the entire message. + while (payload.hasRemaining()) { + if (keySpace != KeySpace.ONE_RTT && + handshakeState != HandshakeState.NEED_RECV_CRYPTO) { + // in one-rtt we may receive session tickets at any time; + // during handshake we're either sending or receiving + throw new QuicTransportException("Unexpected message", null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException( + "Not expecting a handshake message, state: " + + handshakeState)); + } + if (incomingCryptoBuffer != null) { + // message type validated already; pump more bytes + if (payload.remaining() <= incomingCryptoBuffer.remaining()) { + incomingCryptoBuffer.put(payload); + } else { + // more than one message in buffer, or we don't have a + // header yet + int remaining = incomingCryptoBuffer.remaining(); + incomingCryptoBuffer.put(incomingCryptoBuffer.position(), + payload, payload.position(), remaining); + incomingCryptoBuffer.position(incomingCryptoBuffer.limit()); + payload.position(payload.position() + remaining); + if (incomingCryptoBuffer.capacity() == 4) { + // small buffer for header only; retrieve size and + // expand if necessary + int messageSize = + ((incomingCryptoBuffer.get(1) & 0xFF) << 16) | + ((incomingCryptoBuffer.get(2) & 0xFF) << 8) | + (incomingCryptoBuffer.get(3) & 0xFF); + if (messageSize != 0) { + if (messageSize > SSLConfiguration.maxHandshakeMessageSize) { + throw new QuicTransportException( + "The size of the handshake message (" + + messageSize + + ") exceeds the maximum allowed size (" + + SSLConfiguration.maxHandshakeMessageSize + + ")", + null, 0, + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + ByteBuffer newBuffer = + ByteBuffer.allocate(messageSize + 4); + incomingCryptoBuffer.flip(); + newBuffer.put(incomingCryptoBuffer); + incomingCryptoBuffer = newBuffer; + assert incomingCryptoBuffer.position() == 4 : + incomingCryptoBuffer.position(); + // start over with larger buffer + continue; + } + // message size was zero... can it really happen? + } + } + } else { + // incoming crypto buffer is null. Validate message type, + // check if size is available + byte messageType = payload.get(payload.position()); + if (SSLLogger.isOn) { + SSLLogger.fine("Received message of type 0x" + + Integer.toHexString(messageType & 0xFF)); + } + KeySpace expected = messageTypeMap.get(messageType); + if (expected != keySpace) { + throw new QuicTransportException("Unexpected message", + null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException("Message " + messageType + + " received in " + keySpace + + " but should be " + expected)); + } + if (payload.remaining() < 4) { + // partial message, length missing. Store in + // incomingCryptoBuffer + incomingCryptoBuffer = ByteBuffer.allocate(4); + incomingCryptoBuffer.put(payload); + incomingCryptoSpace = keySpace; + return; + } + int payloadPos = payload.position(); + int messageSize = ((payload.get(payloadPos + 1) & 0xFF) << 16) + | ((payload.get(payloadPos + 2) & 0xFF) << 8) + | (payload.get(payloadPos + 3) & 0xFF); + if (payload.remaining() < messageSize + 4) { + // partial message, length known. Store in + // incomingCryptoBuffer + if (messageSize > SSLConfiguration.maxHandshakeMessageSize) { + throw new QuicTransportException( + "The size of the handshake message (" + + messageSize + + ") exceeds the maximum allowed size (" + + SSLConfiguration.maxHandshakeMessageSize + + ")", + null, 0, + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + incomingCryptoBuffer = ByteBuffer.allocate(messageSize + 4); + incomingCryptoBuffer.put(payload); + incomingCryptoSpace = keySpace; + return; + } + incomingCryptoSpace = keySpace; + incomingCryptoBuffer = payload.slice(payloadPos, + messageSize + 4); + // set position at end to indicate that the buffer is ready + // for processing + incomingCryptoBuffer.position(messageSize + 4); + assert !incomingCryptoBuffer.hasRemaining() : + incomingCryptoBuffer.remaining(); + payload.position(payloadPos + messageSize + 4); + } + if (!incomingCryptoBuffer.hasRemaining()) { + incomingCryptoBuffer.flip(); + handleHandshakeMessage(keySpace, incomingCryptoBuffer); + incomingCryptoBuffer = null; + incomingCryptoSpace = null; + } else { + assert !payload.hasRemaining() : payload.remaining(); + return; + } + } + } + + private void handleHandshakeMessage(KeySpace keySpace, ByteBuffer message) + throws QuicTransportException { + // message param contains one whole TLS message + boolean useClientMode = getUseClientMode(); + byte messageType = message.get(); + int messageSize = ((message.get() & 0xFF) << 16) + | ((message.get() & 0xFF) << 8) + | (message.get() & 0xFF); + + assert message.remaining() == messageSize : + message.remaining() - messageSize; + try { + if (conContext.inputRecord.handshakeHash.isHashable(messageType)) { + ByteBuffer temp = message.duplicate(); + temp.position(0); + conContext.inputRecord.handshakeHash.receive(temp); + } + if (conContext.handshakeContext == null) { + if (!conContext.isNegotiated) { + throw new QuicTransportException( + "Cannot process crypto message, broken: " + + conContext.isBroken, + null, 0, QuicTransportErrors.INTERNAL_ERROR); + } + conContext.handshakeContext = + new PostHandshakeContext(conContext); + } + conContext.handshakeContext.dispatch(messageType, message.slice()); + } catch (SSLHandshakeException e) { + if (e.getCause() instanceof QuicTransportException qte) { + // rethrow quic transport parameters validation exception + throw qte; + } + Alert alert = ((QuicEngineOutputRecord) + conContext.outputRecord).getAlert(); + throw new QuicTransportException(alert.description, keySpace, 0, + BASE_CRYPTO_ERROR + alert.id, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (handshakeState == NEED_RECV_CRYPTO) { + if (conContext.outputRecord.isEmpty()) { + if (conContext.isNegotiated) { + // dead code? done, server side, no session ticket + handshakeState = NEED_SEND_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + // expect more messages + // client side: if we're still in INITIAL, switch + // to HANDSHAKE + if (sendKeySpace == INITIAL) { + sendKeySpace = HANDSHAKE; + } + } + } else { + // our turn to send + if (conContext.isNegotiated && !useClientMode) { + // done, server side, wants to send session ticket + handshakeState = NEED_SEND_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + // more messages needed to finish handshake + handshakeState = HandshakeState.NEED_SEND_CRYPTO; + } + } + } else { + assert conContext.isNegotiated; + } + } + + @Override + public void deriveInitialKeys(final QuicVersion quicVersion, + final ByteBuffer connectionId) throws IOException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException("Quic version " + quicVersion + + " isn't enabled"); + } + final byte[] connectionIdBytes = new byte[connectionId.remaining()]; + connectionId.get(connectionIdBytes); + this.initialKeyManager.deriveKeys(quicVersion, connectionIdBytes, + getUseClientMode()); + } + + @Override + public void versionNegotiated(final QuicVersion quicVersion) { + Objects.requireNonNull(quicVersion); + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException("Quic version " + quicVersion + + " is not enabled"); + } + synchronized (this) { + final QuicVersion prevNegotiated = this.negotiatedVersion; + if (prevNegotiated != null) { + throw new IllegalStateException("A Quic version has already " + + "been negotiated previously"); + } + this.negotiatedVersion = quicVersion; + } + } + + public void deriveHandshakeKeys() throws IOException { + final QuicVersion quicVersion = getNegotiatedVersion(); + this.handshakeKeyManager.deriveKeys(quicVersion, + this.conContext.handshakeContext, + getUseClientMode()); + } + + public void deriveOneRTTKeys() throws IOException { + final QuicVersion quicVersion = getNegotiatedVersion(); + this.oneRttKeyManager.deriveKeys(quicVersion, + this.conContext.handshakeContext, + getUseClientMode()); + } + + // for testing (PacketEncryptionTest) + void deriveOneRTTKeys(final QuicVersion version, + final SecretKey client_application_traffic_secret_0, + final SecretKey server_application_traffic_secret_0, + final CipherSuite negotiatedCipherSuite, + final boolean clientMode) throws IOException, + GeneralSecurityException { + this.oneRttKeyManager.deriveOneRttKeys(version, + client_application_traffic_secret_0, + server_application_traffic_secret_0, + negotiatedCipherSuite, clientMode); + } + + @Override + public Runnable getDelegatedTask() { + // TODO: actually delegate tasks + return null; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public int getPeerPort() { + return peerPort; + } + + @Override + public boolean useDelegatedTask() { + return true; + } + + public byte[] getLocalQuicTransportParameters() { + ByteBuffer ltp = localQuicTransportParameters; + if (ltp == null) { + return null; + } + byte[] result = new byte[ltp.remaining()]; + ltp.get(0, result); + return result; + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + this.localQuicTransportParameters = params; + } + + @Override + public void restartHandshake() throws IOException { + if (negotiatedVersion != null) { + throw new IllegalStateException("Version already negotiated"); + } + if (sendKeySpace != INITIAL || handshakeState != NEED_RECV_CRYPTO) { + throw new IllegalStateException("Unexpected handshake state"); + } + HandshakeContext context = conContext.handshakeContext; + ClientHandshakeContext chc = (ClientHandshakeContext)context; + + // Refresh handshake hash + chc.handshakeHash.finish(); // reset the handshake hash + + // Update the initial ClientHello handshake message. + chc.initialClientHelloMsg.extensions.reproduce(chc, + new SSLExtension[] { + SSLExtension.CH_QUIC_TRANSPORT_PARAMETERS, + SSLExtension.CH_PRE_SHARED_KEY + }); + + // produce handshake message + chc.initialClientHelloMsg.write(chc.handshakeOutput); + handshakeState = NEED_SEND_CRYPTO; + } + + @Override + public void setRemoteQuicTransportParametersConsumer( + QuicTransportParametersConsumer consumer) { + this.remoteQuicTransportParametersConsumer = consumer; + } + + void processRemoteQuicTransportParameters(ByteBuffer buffer) + throws QuicTransportException{ + remoteQuicTransportParametersConsumer.accept(buffer); + } + + @Override + public boolean tryMarkHandshakeDone() { + if (getUseClientMode()) { + // not expected to be called on client + throw new IllegalStateException( + "Not expected to be called in client mode"); + } + final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this, + NEED_SEND_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED); + if (confirmed) { + if (SSLLogger.isOn) { + SSLLogger.fine("QuicTLSEngine (server) marked handshake " + + "state as HANDSHAKE_CONFIRMED"); + } + } + return confirmed; + } + + @Override + public boolean tryReceiveHandshakeDone() { + final boolean isClient = getUseClientMode(); + if (!isClient) { + throw new IllegalStateException( + "Not expected to receive HANDSHAKE_DONE in server mode"); + } + final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this, + NEED_RECV_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED); + if (confirmed) { + if (SSLLogger.isOn) { + SSLLogger.fine( + "QuicTLSEngine (client) received HANDSHAKE_DONE," + + " marking state as HANDSHAKE_DONE"); + } + } + return confirmed; + } + + @Override + public boolean isTLSHandshakeComplete() { + final boolean isClient = getUseClientMode(); + final HandshakeState hsState = this.handshakeState; + if (isClient) { + // the client has received TLS Finished message from server and + // has sent its own TLS Finished message and is waiting for the server + // to send QUIC HANDSHAKE_DONE frame. + // OR + // the client has received TLS Finished message from server and + // has sent its own TLS Finished message and has even received the + // QUIC HANDSHAKE_DONE frame. + // Either of these implies the TLS handshake is complete for the client + return hsState == NEED_RECV_HANDSHAKE_DONE || hsState == HANDSHAKE_CONFIRMED; + } + // on the server side the TLS handshake is complete only when the server has + // sent a TLS Finished message and received the client's Finished message. + return hsState == HANDSHAKE_CONFIRMED; + } + + /** + * {@return the key phase being used when decrypting incoming 1-RTT + * packets} + */ + // this is only used in tests + public int getOneRttKeyPhase() throws QuicKeyUnavailableException { + return this.oneRttKeyManager.getReadCipher().getKeyPhase(); + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java new file mode 100644 index 00000000000..83e977ee446 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.ssl; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jdk.internal.net.quic.QuicTransportException; +import sun.security.ssl.SSLExtension.ExtensionConsumer; +import sun.security.ssl.SSLHandshake.HandshakeMessage; + +/** + * Pack of the "quic_transport_parameters" extensions [RFC 9001]. + */ +final class QuicTransportParametersExtension { + + static final HandshakeProducer chNetworkProducer = + new T13CHQuicParametersProducer(); + static final ExtensionConsumer chOnLoadConsumer = + new T13CHQuicParametersConsumer(); + static final HandshakeAbsence chOnLoadAbsence = + new T13CHQuicParametersAbsence(); + static final HandshakeProducer eeNetworkProducer = + new T13EEQuicParametersProducer(); + static final ExtensionConsumer eeOnLoadConsumer = + new T13EEQuicParametersConsumer(); + static final HandshakeAbsence eeOnLoadAbsence = + new T13EEQuicParametersAbsence(); + + private static final class T13CHQuicParametersProducer + implements HandshakeProducer { + // Prevent instantiation of this class. + private T13CHQuicParametersProducer() { + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) throws IOException { + + ClientHandshakeContext chc = (ClientHandshakeContext) context; + if (!chc.sslConfig.isQuic) { + return null; + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) chc.conContext.transport; + + return quicTLSEngine.getLocalQuicTransportParameters(); + } + + } + + private static final class T13CHQuicParametersConsumer + implements ExtensionConsumer { + // Prevent instantiation of this class. + private T13CHQuicParametersConsumer() { + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + ServerHandshakeContext shc = (ServerHandshakeContext) context; + if (!shc.sslConfig.isQuic) { + throw shc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION, + "Client sent the quic_transport_parameters " + + "extension in a non-QUIC context"); + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + quicTLSEngine.processRemoteQuicTransportParameters(buffer); + } catch (QuicTransportException e) { + throw shc.conContext.fatal(Alert.DECODE_ERROR, e); + } + + } + } + + private static final class T13CHQuicParametersAbsence + implements HandshakeAbsence { + // Prevent instantiation of this class. + private T13CHQuicParametersAbsence() { + } + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) throws IOException { + // The producing happens in server side only. + ServerHandshakeContext shc = (ServerHandshakeContext)context; + + if (shc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST send quic_transport_parameters + throw shc.conContext.fatal( + Alert.MISSING_EXTENSION, + "Client did not send QUIC transport parameters"); + } + } + } + + private static final class T13EEQuicParametersProducer + implements HandshakeProducer { + // Prevent instantiation of this class. + private T13EEQuicParametersProducer() { + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) { + + ServerHandshakeContext shc = (ServerHandshakeContext) context; + if (!shc.sslConfig.isQuic) { + return null; + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) shc.conContext.transport; + + return quicTLSEngine.getLocalQuicTransportParameters(); + } + } + + private static final class T13EEQuicParametersConsumer + implements ExtensionConsumer { + // Prevent instantiation of this class. + private T13EEQuicParametersConsumer() { + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + if (!chc.sslConfig.isQuic) { + throw chc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION, + "Server sent the quic_transport_parameters " + + "extension in a non-QUIC context"); + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + quicTLSEngine.processRemoteQuicTransportParameters(buffer); + } catch (QuicTransportException e) { + throw chc.conContext.fatal(Alert.DECODE_ERROR, e); + } + } + } + + private static final class T13EEQuicParametersAbsence + implements HandshakeAbsence { + // Prevent instantiation of this class. + private T13EEQuicParametersAbsence() { + } + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) throws IOException { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + + if (chc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST send quic_transport_parameters + throw chc.conContext.fatal( + Alert.MISSING_EXTENSION, + "Server did not send QUIC transport parameters"); + } + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java index 88cdfbca5ff..1d5a4c4e73d 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java @@ -29,19 +29,32 @@ import java.security.AlgorithmConstraints; import java.security.AlgorithmParameters; import java.security.CryptoPrimitive; import java.security.Key; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Set; +import java.util.TreeSet; import javax.net.ssl.*; + +import jdk.internal.net.quic.QuicTLSEngine; import sun.security.util.DisabledAlgorithmConstraints; import static sun.security.util.DisabledAlgorithmConstraints.*; /** * Algorithm constraints for disabled algorithms property - * + *

    * See the "jdk.certpath.disabledAlgorithms" specification in java.security * for the syntax of the disabled algorithm string. */ final class SSLAlgorithmConstraints implements AlgorithmConstraints { + public enum SIGNATURE_CONSTRAINTS_MODE { + PEER, // Check against peer supported signatures + LOCAL // Check against local supported signatures + } + private static final DisabledAlgorithmConstraints tlsDisabledAlgConstraints = new DisabledAlgorithmConstraints(PROPERTY_TLS_DISABLED_ALGS, new SSLAlgorithmDecomposer()); @@ -57,14 +70,15 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // the default algorithm constraints static final SSLAlgorithmConstraints DEFAULT = - new SSLAlgorithmConstraints(null, true); + new SSLAlgorithmConstraints(null, true); // the default SSL only algorithm constraints static final SSLAlgorithmConstraints DEFAULT_SSL_ONLY = - new SSLAlgorithmConstraints(null, false); + new SSLAlgorithmConstraints(null, false); - private SSLAlgorithmConstraints(AlgorithmConstraints userSpecifiedConstraints, - boolean enabledX509DisabledAlgConstraints) { + private SSLAlgorithmConstraints( + AlgorithmConstraints userSpecifiedConstraints, + boolean enabledX509DisabledAlgConstraints) { this(userSpecifiedConstraints, null, enabledX509DisabledAlgConstraints); } @@ -81,10 +95,12 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the provided * {@code userSpecifiedConstraints} in addition to standard checks. * Returns a singleton instance if parameter is null or DEFAULT. + * * @param userSpecifiedConstraints additional constraints to check * @return a SSLAlgorithmConstraints instance */ - static SSLAlgorithmConstraints wrap(AlgorithmConstraints userSpecifiedConstraints) { + static SSLAlgorithmConstraints wrap( + AlgorithmConstraints userSpecifiedConstraints) { return wrap(userSpecifiedConstraints, true); } @@ -102,23 +118,24 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the constraints * configured for the given {@code socket} in addition to standard checks. * Returns a singleton instance if the constraints are null or DEFAULT. + * * @param socket socket with configured constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE * @return a SSLAlgorithmConstraints instance */ - static AlgorithmConstraints forSocket(SSLSocket socket, - boolean withDefaultCertPathConstraints) { - AlgorithmConstraints userSpecifiedConstraints = - getUserSpecifiedConstraints(socket); - return wrap(userSpecifiedConstraints, withDefaultCertPathConstraints); - } - static SSLAlgorithmConstraints forSocket( SSLSocket socket, - String[] supportedAlgorithms, + SIGNATURE_CONSTRAINTS_MODE mode, boolean withDefaultCertPathConstraints) { + + if (socket == null) { + return wrap(null, withDefaultCertPathConstraints); + } + return new SSLAlgorithmConstraints( nullIfDefault(getUserSpecifiedConstraints(socket)), - new SupportedSignatureAlgorithmConstraints(supportedAlgorithms), + new SupportedSignatureAlgorithmConstraints( + socket.getHandshakeSession(), mode), withDefaultCertPathConstraints); } @@ -126,23 +143,51 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the constraints * configured for the given {@code engine} in addition to standard checks. * Returns a singleton instance if the constraints are null or DEFAULT. + * * @param engine engine with configured constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE * @return a SSLAlgorithmConstraints instance */ - static AlgorithmConstraints forEngine(SSLEngine engine, - boolean withDefaultCertPathConstraints) { - AlgorithmConstraints userSpecifiedConstraints = - getUserSpecifiedConstraints(engine); - return wrap(userSpecifiedConstraints, withDefaultCertPathConstraints); - } - static SSLAlgorithmConstraints forEngine( SSLEngine engine, - String[] supportedAlgorithms, + SIGNATURE_CONSTRAINTS_MODE mode, boolean withDefaultCertPathConstraints) { + + if (engine == null) { + return wrap(null, withDefaultCertPathConstraints); + } + return new SSLAlgorithmConstraints( nullIfDefault(getUserSpecifiedConstraints(engine)), - new SupportedSignatureAlgorithmConstraints(supportedAlgorithms), + new SupportedSignatureAlgorithmConstraints( + engine.getHandshakeSession(), mode), + withDefaultCertPathConstraints); + } + + /** + * Returns an {@link AlgorithmConstraints} instance that uses the + * constraints configured for the given {@code engine} in addition + * to the platform configured constraints. + *

    + * If the given {@code allowedAlgorithms} is non-null then the returned + * {@code AlgorithmConstraints} will only permit those allowed algorithms. + * + * @param engine QuicTLSEngine used to determine the constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE + * @param withDefaultCertPathConstraints whether or not to apply the default certpath + * algorithm constraints too + * @return a AlgorithmConstraints instance + */ + static AlgorithmConstraints forQUIC(QuicTLSEngine engine, + SIGNATURE_CONSTRAINTS_MODE mode, + boolean withDefaultCertPathConstraints) { + if (engine == null) { + return wrap(null, withDefaultCertPathConstraints); + } + + return new SSLAlgorithmConstraints( + nullIfDefault(getUserSpecifiedConstraints(engine)), + new SupportedSignatureAlgorithmConstraints(engine.getHandshakeSession(), mode), withDefaultCertPathConstraints); } @@ -159,7 +204,7 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // Please check the instance before casting to use SSLEngineImpl. if (engine instanceof SSLEngineImpl) { HandshakeContext hc = - ((SSLEngineImpl)engine).conContext.handshakeContext; + ((SSLEngineImpl) engine).conContext.handshakeContext; if (hc != null) { return hc.sslConfig.userSpecifiedAlgorithmConstraints; } @@ -179,7 +224,7 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // Please check the instance before casting to use SSLSocketImpl. if (socket instanceof SSLSocketImpl) { HandshakeContext hc = - ((SSLSocketImpl)socket).conContext.handshakeContext; + ((SSLSocketImpl) socket).conContext.handshakeContext; if (hc != null) { return hc.sslConfig.userSpecifiedAlgorithmConstraints; } @@ -191,6 +236,17 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { return null; } + private static AlgorithmConstraints getUserSpecifiedConstraints( + QuicTLSEngine quicEngine) { + if (quicEngine != null) { + if (quicEngine instanceof QuicTLSEngineImpl engineImpl) { + return engineImpl.getAlgorithmConstraints(); + } + return quicEngine.getSSLParameters().getAlgorithmConstraints(); + } + return null; + } + @Override public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { @@ -279,15 +335,55 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { } private static class SupportedSignatureAlgorithmConstraints - implements AlgorithmConstraints { - // supported signature algorithms - private final String[] supportedAlgorithms; + implements AlgorithmConstraints { - SupportedSignatureAlgorithmConstraints(String[] supportedAlgorithms) { - if (supportedAlgorithms != null) { - this.supportedAlgorithms = supportedAlgorithms.clone(); - } else { - this.supportedAlgorithms = null; + // Supported signature algorithms + private Set supportedAlgorithms; + // Supported signature schemes + private List supportedSignatureSchemes; + private boolean checksDisabled; + + SupportedSignatureAlgorithmConstraints( + SSLSession session, SIGNATURE_CONSTRAINTS_MODE mode) { + + if (mode == null + || !(session instanceof ExtendedSSLSession extSession + // "signature_algorithms_cert" TLS extension is only + // available starting with TLSv1.2. + && ProtocolVersion.useTLS12PlusSpec( + extSession.getProtocol()))) { + + checksDisabled = true; + return; + } + + supportedAlgorithms = new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + + switch (mode) { + case SIGNATURE_CONSTRAINTS_MODE.PEER: + supportedAlgorithms.addAll(Arrays.asList(extSession + .getPeerSupportedSignatureAlgorithms())); + break; + case SIGNATURE_CONSTRAINTS_MODE.LOCAL: + supportedAlgorithms.addAll(Arrays.asList(extSession + .getLocalSupportedSignatureAlgorithms())); + } + + // Do additional SignatureSchemes checks for in-house + // ExtendedSSLSession implementation. + if (extSession instanceof SSLSessionImpl sslSessionImpl) { + switch (mode) { + case SIGNATURE_CONSTRAINTS_MODE.PEER: + supportedSignatureSchemes = new ArrayList<>( + sslSessionImpl + .getPeerSupportedSignatureSchemes()); + break; + case SIGNATURE_CONSTRAINTS_MODE.LOCAL: + supportedSignatureSchemes = new ArrayList<>( + sslSessionImpl + .getLocalSupportedSignatureSchemes()); + } } } @@ -295,6 +391,10 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { + if (checksDisabled) { + return true; + } + if (algorithm == null || algorithm.isEmpty()) { throw new IllegalArgumentException( "No algorithm name specified"); @@ -305,24 +405,11 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { "No cryptographic primitive specified"); } - if (supportedAlgorithms == null || - supportedAlgorithms.length == 0) { + if (supportedAlgorithms == null || supportedAlgorithms.isEmpty()) { return false; } - // trim the MGF part: withand - int position = algorithm.indexOf("and"); - if (position > 0) { - algorithm = algorithm.substring(0, position); - } - - for (String supportedAlgorithm : supportedAlgorithms) { - if (algorithm.equalsIgnoreCase(supportedAlgorithm)) { - return true; - } - } - - return false; + return supportedAlgorithms.contains(algorithm); } @Override @@ -339,7 +426,41 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { "No algorithm name specified"); } - return permits(primitives, algorithm, parameters); + return permits(primitives, algorithm, parameters) + && checkRsaSsaPssParams(algorithm, key, parameters); + } + + // Additional check for RSASSA-PSS signature algorithm parameters. + private boolean checkRsaSsaPssParams( + String algorithm, Key key, AlgorithmParameters parameters) { + + if (supportedSignatureSchemes == null + || key == null + || parameters == null + || !"RSASSA-PSS".equalsIgnoreCase(algorithm)) { + return true; + } + + try { + String keyAlg = key.getAlgorithm(); + String paramDigestAlg = parameters.getParameterSpec( + PSSParameterSpec.class).getDigestAlgorithm(); + + return supportedSignatureSchemes.stream().anyMatch(ss -> + ss.algorithm.equalsIgnoreCase(algorithm) + && ss.keyAlgorithm.equalsIgnoreCase(keyAlg) + && ((PSSParameterSpec) ss.signAlgParams.parameterSpec) + .getDigestAlgorithm() + .equalsIgnoreCase(paramDigestAlg)); + + } catch (InvalidParameterSpecException e) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Invalid AlgorithmParameters: " + + parameters + "; Error: " + e.getMessage()); + } + + return true; + } } } } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLCipher.java b/src/java.base/share/classes/sun/security/ssl/SSLCipher.java index d11ffc96b47..4a52a2ea583 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLCipher.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLCipher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1923,9 +1923,9 @@ enum SSLCipher { // remove inner plaintext padding int i = pt.limit() - 1; - for (; i > 0 && pt.get(i) == 0; i--); + for (; i >= pos && pt.get(i) == 0; i--); - if (i < (pos + 1)) { + if (i < pos) { throw new BadPaddingException( "Incorrect inner plaintext: no content type"); } @@ -2441,10 +2441,9 @@ enum SSLCipher { // remove inner plaintext padding int i = pt.limit() - 1; - for (; i > 0 && pt.get(i) == 0; i--) { - // blank - } - if (i < (pos + 1)) { + for (; i >= pos && pt.get(i) == 0; i--); + + if (i < pos) { throw new BadPaddingException( "Incorrect inner plaintext: no content type"); } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java index bb032e019d3..aacac465027 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -78,6 +78,7 @@ final class SSLConfiguration implements Cloneable { boolean noSniExtension; boolean noSniMatcher; + boolean isQuic; // To switch off the extended_master_secret extension. static final boolean useExtendedMasterSecret; @@ -91,7 +92,7 @@ final class SSLConfiguration implements Cloneable { Utilities.getBooleanProperty("jdk.tls.allowLegacyMasterSecret", true); // Use TLS1.3 middlebox compatibility mode. - static final boolean useCompatibilityMode = Utilities.getBooleanProperty( + private static final boolean useCompatibilityMode = Utilities.getBooleanProperty( "jdk.tls.client.useCompatibilityMode", true); // Respond a close_notify alert if receiving close_notify alert. @@ -524,6 +525,14 @@ final class SSLConfiguration implements Cloneable { } } + public boolean isUseCompatibilityMode() { + return useCompatibilityMode && !isQuic; + } + + public void setQuic(boolean quic) { + isQuic = quic; + } + @Override @SuppressWarnings({"unchecked", "CloneDeclaresCloneNotSupported"}) public Object clone() { @@ -567,7 +576,10 @@ final class SSLConfiguration implements Cloneable { */ private static String[] getCustomizedSignatureScheme(String propertyName) { String property = System.getProperty(propertyName); - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + // this method is called from class initializer; logging here + // will occasionally pin threads and deadlock if called from a virtual thread + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "System property " + propertyName + " is set to '" + property + "'"); @@ -595,7 +607,8 @@ final class SSLConfiguration implements Cloneable { if (scheme != null && scheme.isAvailable) { signatureSchemes.add(schemeName); } else { - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "The current installed providers do not " + "support signature scheme: " + schemeName); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java index f09270aa9e6..85dde5b0dbb 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ import java.util.*; import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.*; import sun.security.provider.certpath.AlgorithmChecker; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.validator.Validator; /** @@ -480,6 +481,10 @@ public abstract class SSLContextImpl extends SSLContextSpi { return availableProtocols; } + public boolean isUsableWithQuic() { + return trustManager instanceof X509TrustManagerImpl; + } + /* * The SSLContext implementation for SSL/(D)TLS algorithm * @@ -1449,22 +1454,8 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager identityAlg, checkClientTrusted); } - // try the best to check the algorithm constraints - AlgorithmConstraints constraints; - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - if (session instanceof ExtendedSSLSession extSession) { - String[] peerSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forSocket( - sslSocket, peerSupportedSignAlgs, true); - } else { - constraints = - SSLAlgorithmConstraints.forSocket(sslSocket, true); - } - } else { - constraints = SSLAlgorithmConstraints.forSocket(sslSocket, true); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.LOCAL, true); checkAlgorithmConstraints(chain, constraints, checkClientTrusted); } @@ -1474,6 +1465,7 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager String authType, SSLEngine engine, boolean checkClientTrusted) throws CertificateException { if (engine != null) { + SSLSession session = engine.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); @@ -1487,22 +1479,8 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager identityAlg, checkClientTrusted); } - // try the best to check the algorithm constraints - AlgorithmConstraints constraints; - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - if (session instanceof ExtendedSSLSession extSession) { - String[] peerSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forEngine( - engine, peerSupportedSignAlgs, true); - } else { - constraints = - SSLAlgorithmConstraints.forEngine(engine, true); - } - } else { - constraints = SSLAlgorithmConstraints.forEngine(engine, true); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, true); checkAlgorithmConstraints(chain, constraints, checkClientTrusted); } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java index c7175ea7fdc..082914b4b4b 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java @@ -458,6 +458,28 @@ enum SSLExtension implements SSLStringizer { null, null, null, null, KeyShareExtension.hrrStringizer), + // Extension defined in RFC 9001 + CH_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters", + SSLHandshake.CLIENT_HELLO, + ProtocolVersion.PROTOCOLS_OF_13, + QuicTransportParametersExtension.chNetworkProducer, + QuicTransportParametersExtension.chOnLoadConsumer, + QuicTransportParametersExtension.chOnLoadAbsence, + null, + null, + // TODO properly stringize, rather than hex output. + null), + EE_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters", + SSLHandshake.ENCRYPTED_EXTENSIONS, + ProtocolVersion.PROTOCOLS_OF_13, + QuicTransportParametersExtension.eeNetworkProducer, + QuicTransportParametersExtension.eeOnLoadConsumer, + QuicTransportParametersExtension.eeOnLoadAbsence, + null, + null, + // TODO properly stringize, rather than hex output + null), + // Extensions defined in RFC 5746 (TLS Renegotiation Indication Extension) CH_RENEGOTIATION_INFO (0xff01, "renegotiation_info", SSLHandshake.CLIENT_HELLO, @@ -820,7 +842,10 @@ enum SSLExtension implements SSLStringizer { private static Collection getDisabledExtensions( String propertyName) { String property = System.getProperty(propertyName); - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + // this method is called from class initializer; logging here + // will occasionally pin threads and deadlock if called from a virtual thread + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "System property " + propertyName + " is set to '" + property + "'"); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLLogger.java b/src/java.base/share/classes/sun/security/ssl/SSLLogger.java index 47afee8ddf2..f55ab27d297 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLLogger.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLLogger.java @@ -46,6 +46,7 @@ import sun.security.util.Debug; import sun.security.x509.*; import static java.nio.charset.StandardCharsets.UTF_8; +import static sun.security.ssl.Utilities.LINE_SEP; /** * Implementation of SSL logger. @@ -62,6 +63,7 @@ public final class SSLLogger { private static final String property; public static final boolean isOn; + static { String p = System.getProperty("javax.net.debug"); if (p != null) { @@ -190,7 +192,12 @@ public final class SSLLogger { try { String formatted = SSLSimpleFormatter.formatParameters(params); - logger.log(level, msg, formatted); + // use the customized log method for SSLConsoleLogger + if (logger instanceof SSLConsoleLogger) { + logger.log(level, msg, formatted); + } else { + logger.log(level, msg + ":" + LINE_SEP + formatted); + } } catch (Exception exp) { // ignore it, just for debugging. } @@ -282,7 +289,7 @@ public final class SSLLogger { """, Locale.ENGLISH); - private static final MessageFormat extendedCertFormart = + private static final MessageFormat extendedCertFormat = new MessageFormat( """ "version" : "v{0}", @@ -299,15 +306,6 @@ public final class SSLLogger { """, Locale.ENGLISH); - // - // private static MessageFormat certExtFormat = new MessageFormat( - // "{0} [{1}] '{'\n" + - // " critical: {2}\n" + - // " value: {3}\n" + - // "'}'", - // Locale.ENGLISH); - // - private static final MessageFormat messageFormatNoParas = new MessageFormat( """ @@ -325,7 +323,7 @@ public final class SSLLogger { private static final MessageFormat messageCompactFormatNoParas = new MessageFormat( - "{0}|{1}|{2}|{3}|{4}|{5}|{6}\n", + "{0}|{1}|{2}|{3}|{4}|{5}|{6}" + LINE_SEP, Locale.ENGLISH); private static final MessageFormat messageFormatWithParas = @@ -423,7 +421,7 @@ public final class SSLLogger { if (isFirst) { isFirst = false; } else { - builder.append(",\n"); + builder.append("," + LINE_SEP); } if (parameter instanceof Throwable) { @@ -504,10 +502,10 @@ public final class SSLLogger { if (isFirst) { isFirst = false; } else { - extBuilder.append(",\n"); + extBuilder.append("," + LINE_SEP); } - extBuilder.append("{\n" + - Utilities.indent(certExt.toString()) + "\n}"); + extBuilder.append("{" + LINE_SEP + + Utilities.indent(certExt.toString()) + LINE_SEP +"}"); } Object[] certFields = { x509.getVersion(), @@ -521,7 +519,7 @@ public final class SSLLogger { Utilities.indent(extBuilder.toString()) }; builder.append(Utilities.indent( - extendedCertFormart.format(certFields))); + extendedCertFormat.format(certFields))); } } catch (Exception ce) { // ignore the exception @@ -578,7 +576,7 @@ public final class SSLLogger { // "string c" // ] StringBuilder builder = new StringBuilder(512); - builder.append("\"" + key + "\": [\n"); + builder.append("\"" + key + "\": [" + LINE_SEP); int len = strings.length; for (int i = 0; i < len; i++) { String string = strings[i]; @@ -586,7 +584,7 @@ public final class SSLLogger { if (i != len - 1) { builder.append(","); } - builder.append("\n"); + builder.append(LINE_SEP); } builder.append(" ]"); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java index 5eb9f72af46..1bf561c47e6 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java @@ -1388,10 +1388,10 @@ final class SSLSessionImpl extends ExtendedSSLSession { } /** - * Gets an array of supported signature schemes that the local side is + * Gets a collection of supported signature schemes that the local side is * willing to verify. */ - public Collection getLocalSupportedSignatureSchemes() { + Collection getLocalSupportedSignatureSchemes() { return localSupportedSignAlgs; } @@ -1404,6 +1404,15 @@ final class SSLSessionImpl extends ExtendedSSLSession { return SignatureScheme.getAlgorithmNames(peerSupportedSignAlgs); } + /** + * Gets a collection of supported signature schemes that the peer is + * willing to verify. Those are sent with the "signature_algorithms_cert" + * TLS extension. + */ + Collection getPeerSupportedSignatureSchemes() { + return peerSupportedSignAlgs; + } + /** * Obtains a List containing all {@link SNIServerName}s * of the requested Server Name Indication (SNI) extension. diff --git a/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java b/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java index 1db07c77160..5cb78ed44f7 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java @@ -29,13 +29,11 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.ProviderException; -import java.security.spec.AlgorithmParameterSpec; import javax.crypto.KDF; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.HKDFParameterSpec; import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.SSLHandshakeException; import sun.security.internal.spec.TlsKeyMaterialParameterSpec; import sun.security.internal.spec.TlsKeyMaterialSpec; @@ -191,26 +189,26 @@ enum SSLTrafficKeyDerivation implements SSLKeyDerivationGenerator { private enum KeySchedule { // Note that we use enum name as the key name. - TlsKey ("key", false), - TlsIv ("iv", true), - TlsUpdateNplus1 ("traffic upd", false); + TlsKey ("key"), + TlsIv ("iv"), + TlsUpdateNplus1 ("traffic upd"); private final byte[] label; - private final boolean isIv; - KeySchedule(String label, boolean isIv) { + KeySchedule(String label) { this.label = ("tls13 " + label).getBytes(); - this.isIv = isIv; } int getKeyLength(CipherSuite cs) { - if (this == KeySchedule.TlsUpdateNplus1) - return cs.hashAlg.hashLength; - return isIv ? cs.bulkCipher.ivSize : cs.bulkCipher.keySize; + return switch (this) { + case TlsUpdateNplus1 -> cs.hashAlg.hashLength; + case TlsIv -> cs.bulkCipher.ivSize; + case TlsKey -> cs.bulkCipher.keySize; + }; } String getAlgorithm(CipherSuite cs, String algorithm) { - return isIv ? algorithm : cs.bulkCipher.algorithm; + return this == TlsKey ? cs.bulkCipher.algorithm : algorithm; } } diff --git a/src/java.base/share/classes/sun/security/ssl/ServerHello.java b/src/java.base/share/classes/sun/security/ssl/ServerHello.java index d092d6c07de..1d2faa5351f 100644 --- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java @@ -235,7 +235,8 @@ final class ServerHello { serverVersion.name, Utilities.toHexString(serverRandom.randomBytes), sessionId.toString(), - cipherSuite.name + "(" + Utilities.byte16HexString(cipherSuite.id) + ")", + cipherSuite.name + + "(" + Utilities.byte16HexString(cipherSuite.id) + ")", HexFormat.of().toHexDigits(compressionMethod), Utilities.indent(extensions.toString(), " ") }; @@ -534,8 +535,9 @@ final class ServerHello { // consider the handshake extension impact SSLExtension[] enabledExtensions = - shc.sslConfig.getEnabledExtensions( - SSLHandshake.CLIENT_HELLO, shc.negotiatedProtocol); + shc.sslConfig.getEnabledExtensions( + SSLHandshake.CLIENT_HELLO, + shc.negotiatedProtocol); clientHello.extensions.consumeOnTrade(shc, enabledExtensions); shc.negotiatedProtocol = @@ -670,6 +672,17 @@ final class ServerHello { // Update the context for master key derivation. shc.handshakeKeyDerivation = kd; + if (shc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + engine.deriveHandshakeKeys(); + } catch (IOException e) { + // unlikely + throw shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "Failed to derive keys", e); + } + } // Check if the server supports stateless resumption if (sessionCache.statelessEnabled()) { shc.statelessResumption = true; @@ -784,9 +797,9 @@ final class ServerHello { // first handshake message. This may either be after // a ServerHello or a HelloRetryRequest. // (RFC 8446, Appendix D.4) - shc.conContext.outputRecord.changeWriteCiphers( - SSLWriteCipher.nullTlsWriteCipher(), - (clientHello.sessionId.length() != 0)); + if (clientHello.sessionId.length() != 0) { + shc.conContext.outputRecord.encodeChangeCipherSpec(); + } // Stateless, shall we clean up the handshake context as well? shc.handshakeHash.finish(); // forgot about the handshake hash @@ -1366,10 +1379,21 @@ final class ServerHello { // Should use resumption_master_secret for TLS 1.3. // chc.handshakeSession.setMasterSecret(masterSecret); - // Update the context for master key derivation. chc.handshakeKeyDerivation = secretKD; + if (chc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + engine.deriveHandshakeKeys(); + } catch (IOException e) { + // unlikely + throw chc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "Failed to derive keys", e); + } + } + // update the consumers and producers // // The server sends a dummy change_cipher_spec record immediately diff --git a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java index c0d2bea77ca..444af5d6dae 100644 --- a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java @@ -278,8 +278,10 @@ final class SessionTicketExtension { aad.putInt(keyID).put(compressed); c.updateAAD(aad); + // use getOutputSize to avoid a ShortBufferException + // from providers that require oversized buffers. See JDK-8368514. ByteBuffer out = ByteBuffer.allocate( - data.remaining() - GCM_TAG_LEN / 8); + c.getOutputSize(data.remaining())); c.doFinal(data, out); out.flip(); @@ -291,7 +293,7 @@ final class SessionTicketExtension { return out; } catch (Exception e) { if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { - SSLLogger.fine("Decryption failed." + e.getMessage()); + SSLLogger.fine("Decryption failed." + e); } } diff --git a/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java b/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java index a95b31583bb..b298da05e9a 100644 --- a/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java @@ -31,6 +31,7 @@ import static sun.security.ssl.SignatureScheme.HANDSHAKE_SCOPE; import java.io.IOException; import java.nio.ByteBuffer; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -191,21 +192,9 @@ final class SignatureAlgorithmsExtension { // Produce the extension. SignatureScheme.updateHandshakeLocalSupportedAlgs(chc); - int vectorLen = SignatureScheme.sizeInRecord() * - chc.localSupportedSignAlgs.size(); - byte[] extData = new byte[vectorLen + 2]; - ByteBuffer m = ByteBuffer.wrap(extData); - Record.putInt16(m, vectorLen); - for (SignatureScheme ss : chc.localSupportedSignAlgs) { - Record.putInt16(m, ss.id); - } - - // Update the context. - chc.handshakeExtensions.put( + return produceNetworkLoad(chc, SSLExtension.CH_SIGNATURE_ALGORITHMS, - new SignatureSchemesSpec(chc.localSupportedSignAlgs)); - - return extData; + SSLExtension.CH_SIGNATURE_ALGORITHMS_CERT); } } @@ -391,23 +380,11 @@ final class SignatureAlgorithmsExtension { } // Produce the extension. - // localSupportedSignAlgs has been already updated when we - // set the negotiated protocol. - int vectorLen = SignatureScheme.sizeInRecord() - * shc.localSupportedSignAlgs.size(); - byte[] extData = new byte[vectorLen + 2]; - ByteBuffer m = ByteBuffer.wrap(extData); - Record.putInt16(m, vectorLen); - for (SignatureScheme ss : shc.localSupportedSignAlgs) { - Record.putInt16(m, ss.id); - } - - // Update the context. - shc.handshakeExtensions.put( + // localSupportedSignAlgs and localSupportedCertSignAlgs have been + // already updated when we set the negotiated protocol. + return produceNetworkLoad(shc, SSLExtension.CR_SIGNATURE_ALGORITHMS, - new SignatureSchemesSpec(shc.localSupportedSignAlgs)); - - return extData; + SSLExtension.CR_SIGNATURE_ALGORITHMS_CERT); } } @@ -546,4 +523,45 @@ final class SignatureAlgorithmsExtension { hc.handshakeSession.setPeerSupportedSignatureAlgorithms(certSS); } } + + /** + * Produce network load and update context. + * + * @param hc HandshakeContext + * @param signatureAlgorithmsExt "signature_algorithms" extension + * @param signatureAlgorithmsCertExt "signature_algorithms_cert" + * extension + * @return network load as byte array + */ + private static byte[] produceNetworkLoad( + HandshakeContext hc, SSLExtension signatureAlgorithmsExt, + SSLExtension signatureAlgorithmsCertExt) throws IOException { + + List sigAlgs; + + // If we don't produce "signature_algorithms_cert" extension, then + // the "signature_algorithms" extension should contain signatures + // supported for both: handshake signatures and certificate signatures. + if (hc.sslConfig.isAvailable(signatureAlgorithmsCertExt)) { + sigAlgs = hc.localSupportedSignAlgs; + } else { + sigAlgs = new ArrayList<>(hc.localSupportedSignAlgs); + sigAlgs.retainAll(hc.localSupportedCertSignAlgs); + } + + int vectorLen = SignatureScheme.sizeInRecord() * sigAlgs.size(); + byte[] extData = new byte[vectorLen + 2]; + ByteBuffer m = ByteBuffer.wrap(extData); + Record.putInt16(m, vectorLen); + + for (SignatureScheme ss : sigAlgs) { + Record.putInt16(m, ss.id); + } + + // Update the context. + hc.handshakeExtensions.put( + signatureAlgorithmsExt, new SignatureSchemesSpec(sigAlgs)); + + return extData; + } } diff --git a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java index b3ed5810c56..043a0d84c61 100644 --- a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java +++ b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java @@ -146,12 +146,12 @@ enum SignatureScheme { "RSA", 511, ProtocolVersion.PROTOCOLS_TO_12); - final int id; // hash + signature - final String name; // literal name - private final String algorithm; // signature algorithm - final String keyAlgorithm; // signature key algorithm - private final SigAlgParamSpec signAlgParams; // signature parameters - private final NamedGroup namedGroup; // associated named group + final int id; // hash + signature + final String name; // literal name + final String algorithm; // signature algorithm + final String keyAlgorithm; // signature key algorithm + final SigAlgParamSpec signAlgParams; // signature parameters + private final NamedGroup namedGroup; // associated named group // The minimal required key size in bits. // @@ -185,7 +185,7 @@ enum SignatureScheme { RSA_PSS_SHA384 ("SHA-384", 48), RSA_PSS_SHA512 ("SHA-512", 64); - private final AlgorithmParameterSpec parameterSpec; + final AlgorithmParameterSpec parameterSpec; private final AlgorithmParameters parameters; private final boolean isAvailable; diff --git a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java index 2441ad91fde..6bf138f4e45 100644 --- a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java @@ -195,6 +195,13 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking { getAlgorithmConstraints(engine), null, null); } + @Override + String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT, + getAlgorithmConstraints(quicTLSEngine), null, null); + } + /* * Choose an alias to authenticate the server side of a secure * socket given the public key type and the list of @@ -222,6 +229,16 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking { X509TrustManagerImpl.getRequestedServerNames(engine), "HTTPS"); } + @Override + String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER, + getAlgorithmConstraints(quicTLSEngine), + X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine), + "HTTPS"); + } + /* * Get the matching aliases for authenticating the client side of a secure * socket given the public key type and the list of diff --git a/src/java.base/share/classes/sun/security/ssl/TransportContext.java b/src/java.base/share/classes/sun/security/ssl/TransportContext.java index 717c81723ff..49fd664e9ed 100644 --- a/src/java.base/share/classes/sun/security/ssl/TransportContext.java +++ b/src/java.base/share/classes/sun/security/ssl/TransportContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -489,6 +489,10 @@ final class TransportContext implements ConnectionContext { isUnsureMode = false; } + public void setQuic(boolean quic) { + sslConfig.setQuic(quic); + } + // The OutputRecord is closed and not buffered output record. boolean isOutboundDone() { return outputRecord.isClosed() && outputRecord.isEmpty(); diff --git a/src/java.base/share/classes/sun/security/ssl/Utilities.java b/src/java.base/share/classes/sun/security/ssl/Utilities.java index 3ed022db382..50cd3bee751 100644 --- a/src/java.base/share/classes/sun/security/ssl/Utilities.java +++ b/src/java.base/share/classes/sun/security/ssl/Utilities.java @@ -40,6 +40,7 @@ final class Utilities { Pattern.compile("\\r\\n|\\n|\\r"); private static final HexFormat HEX_FORMATTER = HexFormat.of().withUpperCase(); + static final String LINE_SEP = System.lineSeparator(); /** * Puts {@code hostname} into the {@code serverNames} list. @@ -150,7 +151,7 @@ final class Utilities { static String indent(String source, String prefix) { StringBuilder builder = new StringBuilder(); if (source == null) { - builder.append("\n").append(prefix).append(""); + builder.append(LINE_SEP).append(prefix).append(""); } else { String[] lines = lineBreakPatern.split(source); boolean isFirst = true; @@ -158,7 +159,7 @@ final class Utilities { if (isFirst) { isFirst = false; } else { - builder.append("\n"); + builder.append(LINE_SEP); } builder.append(prefix).append(line); } diff --git a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java index 4e91df2806e..5abc2cb1bf4 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java +++ b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -218,6 +218,28 @@ enum X509Authentication implements SSLAuthentication { chc.peerSupportedAuthorities == null ? null : chc.peerSupportedAuthorities.clone(), engine); + } else if (chc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) { + // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that + // takes QuicTLSEngine. + // For now, in context of QUIC, for KeyManager implementations other than + // subclasses of sun.security.ssl.X509KeyManagerCertChecking + // we don't take into account + // any algorithm constraints when choosing the client alias and + // just call the functionally limited + // javax.net.ssl.X509KeyManager.chooseClientAlias(...) + if (km instanceof X509KeyManagerCertChecking xkm) { + clientAlias = xkm.chooseQuicClientAlias(keyTypes, + chc.peerSupportedAuthorities == null + ? null + : chc.peerSupportedAuthorities.clone(), + quicEngineImpl); + } else { + clientAlias = km.chooseClientAlias(keyTypes, + chc.peerSupportedAuthorities == null + ? null + : chc.peerSupportedAuthorities.clone(), + null); + } } if (clientAlias == null) { @@ -290,6 +312,28 @@ enum X509Authentication implements SSLAuthentication { shc.peerSupportedAuthorities == null ? null : shc.peerSupportedAuthorities.clone(), engine); + } else if (shc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) { + // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that + // takes QuicTLSEngine. + // For now, in context of QUIC, for KeyManager implementations other than + // subclasses of sun.security.ssl.X509KeyManagerCertChecking + // we don't take into account + // any algorithm constraints when choosing the server alias + // and just call the functionally limited + // javax.net.ssl.X509KeyManager.chooseServerAlias(...) + if (km instanceof X509KeyManagerCertChecking xkm) { + serverAlias = xkm.chooseQuicServerAlias(keyType, + shc.peerSupportedAuthorities == null + ? null + : shc.peerSupportedAuthorities.clone(), + quicEngineImpl); + } else { + serverAlias = km.chooseServerAlias(keyType, + shc.peerSupportedAuthorities == null + ? null + : shc.peerSupportedAuthorities.clone(), + null); + } } if (serverAlias == null) { diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java index 00a7ae84352..9484ab4f830 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java @@ -39,16 +39,15 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.StandardConstants; import javax.net.ssl.X509ExtendedKeyManager; import javax.security.auth.x500.X500Principal; import sun.security.provider.certpath.AlgorithmChecker; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.KnownOIDs; import sun.security.validator.Validator; @@ -75,6 +74,15 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { abstract boolean isCheckingDisabled(); + // TODO move this method to a public interface / class + abstract String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine); + + // TODO move this method to a public interface / class + abstract String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine); + // Entry point to do all certificate checks. protected EntryStatus checkAlias(int keyStoreIndex, String alias, Certificate[] chain, Date verificationDate, List keyTypes, @@ -167,28 +175,9 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { return null; } - if (socket != null && socket.isConnected() && - socket instanceof SSLSocket sslSocket) { - - SSLSession session = sslSocket.getHandshakeSession(); - - if (session != null) { - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - String[] peerSupportedSignAlgs = null; - - if (session instanceof ExtendedSSLSession extSession) { - // Peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - peerSupportedSignAlgs = - extSession.getPeerSupportedSignatureAlgorithms(); - } - - return SSLAlgorithmConstraints.forSocket( - sslSocket, peerSupportedSignAlgs, true); - } - } - - return SSLAlgorithmConstraints.forSocket(sslSocket, true); + if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { + return SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } return SSLAlgorithmConstraints.DEFAULT; @@ -201,26 +190,19 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { return null; } - if (engine != null) { - SSLSession session = engine.getHandshakeSession(); - if (session != null) { - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - String[] peerSupportedSignAlgs = null; + return SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); + } - if (session instanceof ExtendedSSLSession extSession) { - // Peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - peerSupportedSignAlgs = - extSession.getPeerSupportedSignatureAlgorithms(); - } + // Gets algorithm constraints of QUIC TLS engine. + protected AlgorithmConstraints getAlgorithmConstraints(QuicTLSEngineImpl engine) { - return SSLAlgorithmConstraints.forEngine( - engine, peerSupportedSignAlgs, true); - } - } + if (checksDisabled) { + return null; } - return SSLAlgorithmConstraints.forEngine(engine, true); + return SSLAlgorithmConstraints.forQUIC( + engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } // Algorithm constraints check. diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java index df6ecaf7a42..e48096cc363 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java @@ -129,6 +129,13 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { getAlgorithmConstraints(engine), null, null); } + @Override + String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT, + getAlgorithmConstraints(quicTLSEngine), null, null); + } + @Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { @@ -165,6 +172,16 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { // It is not a really HTTPS endpoint identification. } + @Override + String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER, + getAlgorithmConstraints(quicTLSEngine), + X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine), + "HTTPS"); + } + @Override public String[] getClientAliases(String keyType, Principal[] issuers) { return getAliases(keyType, issuers, CheckType.CLIENT); diff --git a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java index 58794e5dce8..d82b94a1d7d 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,7 +30,10 @@ import java.security.*; import java.security.cert.*; import java.util.*; import java.util.concurrent.locks.ReentrantLock; + import javax.net.ssl.*; + +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.AnchorCertificates; import sun.security.util.HostnameChecker; import sun.security.validator.*; @@ -144,6 +147,16 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager checkTrusted(chain, authType, engine, false); } + public void checkClientTrusted(X509Certificate[] chain, String authType, + QuicTLSEngineImpl quicTLSEngine) throws CertificateException { + checkTrusted(chain, authType, quicTLSEngine, true); + } + + void checkServerTrusted(X509Certificate[] chain, String authType, + QuicTLSEngineImpl quicTLSEngine) throws CertificateException { + checkTrusted(chain, authType, quicTLSEngine, false); + } + private Validator checkTrustedInit(X509Certificate[] chain, String authType, boolean checkClientTrusted) { if (chain == null || chain.length == 0) { @@ -198,32 +211,19 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager Validator v = checkTrustedInit(chain, authType, checkClientTrusted); X509Certificate[] trustedChain; - if ((socket != null) && socket.isConnected() && - (socket instanceof SSLSocket sslSocket)) { + if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { SSLSession session = sslSocket.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); } - // create the algorithm constraints - boolean isExtSession = (session instanceof ExtendedSSLSession); - AlgorithmConstraints constraints; - if (isExtSession && - ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - ExtendedSSLSession extSession = (ExtendedSSLSession)session; - String[] localSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forSocket( - sslSocket, localSupportedSignAlgs, false); - } else { - constraints = SSLAlgorithmConstraints.forSocket(sslSocket, false); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); // Grab any stapled OCSP responses for use in validation List responseList = Collections.emptyList(); - if (!checkClientTrusted && isExtSession) { + if (!checkClientTrusted && session instanceof ExtendedSSLSession) { responseList = ((ExtendedSSLSession)session).getStatusResponses(); } @@ -248,6 +248,52 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager } } + private void checkTrusted(X509Certificate[] chain, + String authType, QuicTLSEngineImpl quicTLSEngine, + boolean checkClientTrusted) throws CertificateException { + Validator v = checkTrustedInit(chain, authType, checkClientTrusted); + + final X509Certificate[] trustedChain; + if (quicTLSEngine != null) { + + final SSLSession session = quicTLSEngine.getHandshakeSession(); + if (session == null) { + throw new CertificateException("No handshake session"); + } + + // create the algorithm constraints + final AlgorithmConstraints constraints = SSLAlgorithmConstraints.forQUIC( + quicTLSEngine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); + final List responseList; + // grab any stapled OCSP responses for use in validation + if (!checkClientTrusted && + session instanceof ExtendedSSLSession extSession) { + responseList = extSession.getStatusResponses(); + } else { + responseList = Collections.emptyList(); + } + // do the certificate chain validation + trustedChain = v.validate(chain, null, responseList, + constraints, checkClientTrusted ? null : authType); + + // check endpoint identity + String identityAlg = quicTLSEngine.getSSLParameters(). + getEndpointIdentificationAlgorithm(); + if (identityAlg != null && !identityAlg.isEmpty()) { + checkIdentity(session, trustedChain, + identityAlg, checkClientTrusted); + } + } else { + trustedChain = v.validate(chain, null, Collections.emptyList(), + null, checkClientTrusted ? null : authType); + } + + if (SSLLogger.isOn && SSLLogger.isOn("ssl,trustmanager")) { + SSLLogger.fine("Found trusted certificate", + trustedChain[trustedChain.length - 1]); + } + } + private void checkTrusted(X509Certificate[] chain, String authType, SSLEngine engine, boolean checkClientTrusted) throws CertificateException { @@ -255,29 +301,18 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager X509Certificate[] trustedChain; if (engine != null) { + SSLSession session = engine.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); } - // create the algorithm constraints - boolean isExtSession = (session instanceof ExtendedSSLSession); - AlgorithmConstraints constraints; - if (isExtSession && - ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - ExtendedSSLSession extSession = (ExtendedSSLSession)session; - String[] localSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forEngine( - engine, localSupportedSignAlgs, false); - } else { - constraints = SSLAlgorithmConstraints.forEngine(engine, false); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); // Grab any stapled OCSP responses for use in validation List responseList = Collections.emptyList(); - if (!checkClientTrusted && isExtSession) { + if (!checkClientTrusted && session instanceof ExtendedSSLSession) { responseList = ((ExtendedSSLSession)session).getStatusResponses(); } @@ -367,6 +402,13 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager return Collections.emptyList(); } + static List getRequestedServerNames(QuicTLSEngineImpl engine) { + if (engine != null) { + return getRequestedServerNames(engine.getHandshakeSession()); + } + return Collections.emptyList(); + } + private static List getRequestedServerNames( SSLSession session) { if (session instanceof ExtendedSSLSession) { diff --git a/src/java.base/share/classes/sun/security/util/AbstractAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/util/AbstractAlgorithmConstraints.java index dc5b1aafb20..d05fb262fa8 100644 --- a/src/java.base/share/classes/sun/security/util/AbstractAlgorithmConstraints.java +++ b/src/java.base/share/classes/sun/security/util/AbstractAlgorithmConstraints.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,7 +46,16 @@ public abstract class AbstractAlgorithmConstraints // Get algorithm constraints from the specified security property. static Set getAlgorithms(String propertyName) { - String property = Security.getProperty(propertyName); + return getAlgorithms(propertyName, false); + } + + // Get algorithm constraints from the specified security property or + // system property if allowSystemOverride == true. + static Set getAlgorithms(String propertyName, + boolean allowSystemOverride) { + String property = allowSystemOverride ? + SecurityProperties.getOverridableProperty(propertyName) : + Security.getProperty(propertyName); String[] algorithmsInProperty = null; if (property != null && !property.isEmpty()) { @@ -65,7 +74,8 @@ public abstract class AbstractAlgorithmConstraints if (algorithmsInProperty == null) { return Collections.emptySet(); } - Set algorithmsInPropertySet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + Set algorithmsInPropertySet = + new TreeSet<>(String.CASE_INSENSITIVE_ORDER); algorithmsInPropertySet.addAll(Arrays.asList(algorithmsInProperty)); return algorithmsInPropertySet; } @@ -80,17 +90,17 @@ public abstract class AbstractAlgorithmConstraints return false; } - // decompose the algorithm into sub-elements - Set elements = decomposer.decompose(algorithm); + if (decomposer != null) { + // decompose the algorithm into sub-elements + Set elements = decomposer.decompose(algorithm); - // check the element of the elements - for (String element : elements) { - if (algorithms.contains(element)) { - return false; + // check the element of the elements + for (String element : elements) { + if (algorithms.contains(element)) { + return false; + } } } - return true; } - } diff --git a/src/java.base/share/classes/sun/security/util/CryptoAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/util/CryptoAlgorithmConstraints.java new file mode 100644 index 00000000000..a8649106a38 --- /dev/null +++ b/src/java.base/share/classes/sun/security/util/CryptoAlgorithmConstraints.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.util; + +import java.lang.ref.SoftReference; +import java.security.AlgorithmParameters; +import java.security.CryptoPrimitive; +import java.security.Key; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class implements the algorithm constraints for the + * "jdk.crypto.disabledAlgorithms" security property. This security property + * can be overridden by the system property of the same name. See the + * java.security file for the syntax of the property value. + */ +public class CryptoAlgorithmConstraints extends AbstractAlgorithmConstraints { + private static final Debug debug = Debug.getInstance("jca"); + + // for validating the service + private static final Set SUPPORTED_SERVICES = + Set.of("Cipher", "KeyStore", "MessageDigest", "Signature"); + + // Disabled algorithm security property for JCE crypto services + private static final String PROPERTY_CRYPTO_DISABLED_ALGS = + "jdk.crypto.disabledAlgorithms"; + + private static class CryptoHolder { + static final CryptoAlgorithmConstraints CONSTRAINTS = + new CryptoAlgorithmConstraints(PROPERTY_CRYPTO_DISABLED_ALGS); + } + + private static void debug(String msg) { + if (debug != null) { + debug.println("CryptoAlgoConstraints: ", msg); + } + } + + public static boolean permits(String service, String algo) { + return CryptoHolder.CONSTRAINTS.cachedCheckAlgorithm( + service + "." + algo); + } + + private final Set disabledServices; // syntax is . + private volatile SoftReference> cacheRef = + new SoftReference<>(null); + + /** + * Initialize algorithm constraints with the specified security property + * {@code propertyName}. Note that if a system property of the same name + * is set, it overrides the security property. + * + * @param propertyName the security property name that define the disabled + * algorithm constraints + */ + CryptoAlgorithmConstraints(String propertyName) { + super(null); + disabledServices = getAlgorithms(propertyName, true); + debug("Before " + Arrays.deepToString(disabledServices.toArray())); + for (String dk : disabledServices) { + int idx = dk.indexOf("."); + if (idx < 1 || idx == dk.length() - 1) { + // wrong syntax: missing "." or empty service or algorithm + throw new IllegalArgumentException("Invalid entry: " + dk); + } + String service = dk.substring(0, idx); + String algo = dk.substring(idx + 1); + if (SUPPORTED_SERVICES.stream().anyMatch(e -> e.equalsIgnoreCase + (service))) { + KnownOIDs oid = KnownOIDs.findMatch(algo); + if (oid != null) { + debug("Add oid: " + oid.value()); + disabledServices.add(service + "." + oid.value()); + debug("Add oid stdName: " + oid.stdName()); + disabledServices.add(service + "." + oid.stdName()); + for (String a : oid.aliases()) { + debug("Add oid alias: " + a); + disabledServices.add(service + "." + a); + } + } + } else { + // unsupported service + throw new IllegalArgumentException("Invalid entry: " + dk); + } + } + debug("After " + Arrays.deepToString(disabledServices.toArray())); + } + + @Override + public final boolean permits(Set notUsed1, + String serviceDesc, AlgorithmParameters notUsed2) { + throw new UnsupportedOperationException("Unsupported permits() method"); + } + + @Override + public final boolean permits(Set primitives, Key key) { + throw new UnsupportedOperationException("Unsupported permits() method"); + } + + @Override + public final boolean permits(Set primitives, + String algorithm, Key key, AlgorithmParameters parameters) { + throw new UnsupportedOperationException("Unsupported permits() method"); + } + + // Return false if algorithm is found in the disabledServices Set. + // Otherwise, return true. + private boolean cachedCheckAlgorithm(String serviceDesc) { + Map cache; + if ((cache = cacheRef.get()) == null) { + synchronized (this) { + if ((cache = cacheRef.get()) == null) { + cache = new ConcurrentHashMap<>(); + cacheRef = new SoftReference<>(cache); + } + } + } + Boolean result = cache.get(serviceDesc); + if (result != null) { + return result; + } + result = checkAlgorithm(disabledServices, serviceDesc, null); + cache.put(serviceDesc, result); + return result; + } +} diff --git a/src/java.base/share/classes/sun/security/util/KnownOIDs.java b/src/java.base/share/classes/sun/security/util/KnownOIDs.java index 8e764b75730..cbb0c1e0b57 100644 --- a/src/java.base/share/classes/sun/security/util/KnownOIDs.java +++ b/src/java.base/share/classes/sun/security/util/KnownOIDs.java @@ -184,7 +184,7 @@ public enum KnownOIDs { // RSASecurity // PKCS1 1.2.840.113549.1.1.* PKCS1("1.2.840.113549.1.1", "RSA", false), // RSA KeyPairGenerator and KeyFactory - RSA("1.2.840.113549.1.1.1"), // RSA encryption + RSA("1.2.840.113549.1.1.1", "RSA", "RSA/ECB/PKCS1Padding"), // RSA encryption MD2withRSA("1.2.840.113549.1.1.2"), MD5withRSA("1.2.840.113549.1.1.4"), diff --git a/src/java.base/share/classes/sun/security/util/Password.java b/src/java.base/share/classes/sun/security/util/Password.java index 7acece65a57..e358bcd95de 100644 --- a/src/java.base/share/classes/sun/security/util/Password.java +++ b/src/java.base/share/classes/sun/security/util/Password.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,11 +29,12 @@ import java.io.*; import java.nio.*; import java.nio.charset.*; import java.util.Arrays; + import jdk.internal.access.SharedSecrets; +import jdk.internal.io.JdkConsoleImpl; /** * A utility class for reading passwords - * */ public class Password { /** Reads user password from given input stream. */ @@ -50,30 +51,36 @@ public class Password { char[] consoleEntered = null; byte[] consoleBytes = null; + char[] buf = null; try { // Only use Console if `in` is the initial System.in - Console con; - if (!isEchoOn && - in == SharedSecrets.getJavaLangAccess().initialSystemIn() && - ((con = System.console()) != null)) { - consoleEntered = con.readPassword(); - // readPassword returns "" if you just press ENTER with the built-in Console, - // to be compatible with old Password class, change to null - if (consoleEntered == null || consoleEntered.length == 0) { - return null; + if (!isEchoOn) { + if (in == SharedSecrets.getJavaLangAccess().initialSystemIn() + && ConsoleHolder.consoleIsAvailable()) { + consoleEntered = ConsoleHolder.readPassword(); + // readPassword might return null. Stop now. + if (consoleEntered == null) { + return null; + } + consoleBytes = ConsoleHolder.convertToBytes(consoleEntered); + in = new ByteArrayInputStream(consoleBytes); + } else if (System.in.available() == 0) { + // This may be running in an IDE Run Window or in JShell, + // which acts like an interactive console and echoes the + // entered password. In this case, print a warning that + // the password might be echoed. If available() is not zero, + // it's more likely the input comes from a pipe, such as + // "echo password |" or "cat password_file |" where input + // will be silently consumed without echoing to the screen. + System.err.print(ResourcesMgr.getString + ("warning.input.may.be.visible.on.screen")); } - consoleBytes = convertToBytes(consoleEntered); - in = new ByteArrayInputStream(consoleBytes); } // Rest of the lines still necessary for KeyStoreLoginModule // and when there is no console. - - char[] lineBuffer; - char[] buf; - - buf = lineBuffer = new char[128]; + buf = new char[128]; int room = buf.length; int offset = 0; @@ -101,11 +108,11 @@ public class Password { /* fall through */ default: if (--room < 0) { + char[] oldBuf = buf; buf = new char[offset + 128]; room = buf.length - offset - 1; - System.arraycopy(lineBuffer, 0, buf, 0, offset); - Arrays.fill(lineBuffer, ' '); - lineBuffer = buf; + System.arraycopy(oldBuf, 0, buf, 0, offset); + Arrays.fill(oldBuf, ' '); } buf[offset++] = (char) c; break; @@ -118,8 +125,6 @@ public class Password { char[] ret = new char[offset]; System.arraycopy(buf, 0, ret, 0, offset); - Arrays.fill(buf, ' '); - return ret; } finally { if (consoleEntered != null) { @@ -128,35 +133,72 @@ public class Password { if (consoleBytes != null) { Arrays.fill(consoleBytes, (byte)0); } + if (buf != null) { + Arrays.fill(buf, ' '); + } } } - /** - * Change a password read from Console.readPassword() into - * its original bytes. - * - * @param pass a char[] - * @return its byte[] format, similar to new String(pass).getBytes() - */ - private static byte[] convertToBytes(char[] pass) { - if (enc == null) { - synchronized (Password.class) { - enc = System.console() - .charset() - .newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); + // Everything on Console or JdkConsoleImpl is inside this class. + private static class ConsoleHolder { + + // primary console; may be null + private static final Console c1; + // secondary console (when stdout is redirected); may be null + private static final JdkConsoleImpl c2; + // encoder for c1 or c2 + private static final CharsetEncoder enc; + + static { + c1 = System.console(); + Charset charset; + if (c1 != null) { + c2 = null; + charset = c1.charset(); + } else { + c2 = JdkConsoleImpl.passwordConsole().orElse(null); + charset = (c2 != null) ? c2.charset() : null; + } + enc = charset == null ? null : charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + } + + public static boolean consoleIsAvailable() { + return c1 != null || c2 != null; + } + + public static char[] readPassword() { + assert consoleIsAvailable(); + if (c1 != null) { + return c1.readPassword(); + } else { + try { + return c2.readPasswordNoNewLine(); + } finally { + System.err.println(); + } } } - byte[] ba = new byte[(int)(enc.maxBytesPerChar() * pass.length)]; - ByteBuffer bb = ByteBuffer.wrap(ba); - synchronized (enc) { - enc.reset().encode(CharBuffer.wrap(pass), bb, true); + + /** + * Convert a password read from console into its original bytes. + * + * @param pass a char[] + * @return its byte[] format, equivalent to new String(pass).getBytes() + * but String is immutable and cannot be cleaned up. + */ + public static byte[] convertToBytes(char[] pass) { + assert consoleIsAvailable(); + byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)]; + ByteBuffer bb = ByteBuffer.wrap(ba); + synchronized (enc) { + enc.reset().encode(CharBuffer.wrap(pass), bb, true); + } + if (bb.remaining() > 0) { + bb.put((byte)'\n'); // will be recognized as a stop sign + } + return ba; } - if (bb.position() < ba.length) { - ba[bb.position()] = '\n'; - } - return ba; } - private static volatile CharsetEncoder enc; } diff --git a/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java b/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java index 1910746fe44..987b6967b50 100644 --- a/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java +++ b/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java @@ -32,6 +32,7 @@ import sun.security.util.math.IntegerFieldModuloP; import java.math.BigInteger; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; // Reference: // - [1] Shay Gueron and Vlad Krasnov "Fast Prime Field Elliptic Curve @@ -63,7 +64,7 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial private static final long[] zero = new long[] { 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L }; - private static final long[] modulus = new long[] { + @Stable private static final long[] modulus = new long[] { 0x000fffffffffffffL, 0x00000fffffffffffL, 0x0000000000000000L, 0x0000001000000000L, 0x0000ffffffff0000L }; @@ -207,9 +208,8 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; nn1 = Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); n1 &= LIMB_MASK; - n2 = n * modulus[2]; - nn2 = Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - n2 &= LIMB_MASK; + n2 = 0; + nn2 = 0; n3 = n * modulus[3]; nn3 = Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); n3 &= LIMB_MASK; @@ -221,8 +221,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial d0 += n0; dd1 += nn1; d1 += n1; - dd2 += nn2; - d2 += n2; dd3 += nn3; d3 += n3; dd4 += nn4; @@ -259,9 +257,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -300,9 +295,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -341,9 +333,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -382,9 +371,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -411,7 +397,7 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial c0 = c5 - modulus[0]; c1 = c6 - modulus[1] + (c0 >> BITS_PER_LIMB); c0 &= LIMB_MASK; - c2 = c7 - modulus[2] + (c1 >> BITS_PER_LIMB); + c2 = c7 + (c1 >> BITS_PER_LIMB); c1 &= LIMB_MASK; c3 = c8 - modulus[3] + (c2 >> BITS_PER_LIMB); c2 &= LIMB_MASK; diff --git a/src/java.base/share/classes/sun/security/util/resources/security.properties b/src/java.base/share/classes/sun/security/util/resources/security.properties index 9533b4b8eee..d8a0cbfac29 100644 --- a/src/java.base/share/classes/sun/security/util/resources/security.properties +++ b/src/java.base/share/classes/sun/security/util/resources/security.properties @@ -74,3 +74,6 @@ line.number.expected.expect.found.actual.=line {0}: expected [{1}], found [{2}] # sun.security.pkcs11.SunPKCS11 PKCS11.Token.providerName.Password.=PKCS11 Token [{0}] Password:\u0020 + +# sun.security.util.Password +warning.input.may.be.visible.on.screen=[WARNING: Input may be visible on screen]\u0020 diff --git a/src/java.base/share/classes/sun/security/validator/PKIXValidator.java b/src/java.base/share/classes/sun/security/validator/PKIXValidator.java index 7cbca031cdb..be9d041a4bc 100644 --- a/src/java.base/share/classes/sun/security/validator/PKIXValidator.java +++ b/src/java.base/share/classes/sun/security/validator/PKIXValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -261,7 +261,6 @@ public final class PKIXValidator extends Validator { // apparently issued by trust anchor? X509Certificate last = chain[chain.length - 1]; X500Principal issuer = last.getIssuerX500Principal(); - X500Principal subject = last.getSubjectX500Principal(); if (trustedSubjects.containsKey(issuer)) { return doValidate(chain, pkixParameters); } diff --git a/src/java.base/share/classes/sun/util/locale/LanguageTag.java b/src/java.base/share/classes/sun/util/locale/LanguageTag.java index 6036c1dd04f..0b2fee7f2cd 100644 --- a/src/java.base/share/classes/sun/util/locale/LanguageTag.java +++ b/src/java.base/share/classes/sun/util/locale/LanguageTag.java @@ -34,17 +34,21 @@ package sun.util.locale; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.IllformedLocaleException; import java.util.List; import java.util.Locale; -import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.StringJoiner; // List fields are unmodifiable -public record LanguageTag(String language, String script, String region, String privateuse, - List extlangs, List variants, List extensions) { +public record LanguageTag(String language, + String script, + String region, + String privateuse, + List extlangs, + List variants, + List extensions) { public static final String SEP = "-"; public static final String PRIVATEUSE = "x"; @@ -53,78 +57,6 @@ public record LanguageTag(String language, String script, String region, String private static final String EMPTY_SUBTAG = ""; private static final List EMPTY_SUBTAGS = List.of(); - // Map contains legacy language tags and its preferred mappings from - // http://www.ietf.org/rfc/rfc5646.txt - // Keys are lower-case strings. - private static final Map LEGACY; - - static { - // grandfathered = irregular ; non-redundant tags registered - // / regular ; during the RFC 3066 era - // - // irregular = "en-GB-oed" ; irregular tags do not match - // / "i-ami" ; the 'langtag' production and - // / "i-bnn" ; would not otherwise be - // / "i-default" ; considered 'well-formed' - // / "i-enochian" ; These tags are all valid, - // / "i-hak" ; but most are deprecated - // / "i-klingon" ; in favor of more modern - // / "i-lux" ; subtags or subtag - // / "i-mingo" ; combination - // / "i-navajo" - // / "i-pwn" - // / "i-tao" - // / "i-tay" - // / "i-tsu" - // / "sgn-BE-FR" - // / "sgn-BE-NL" - // / "sgn-CH-DE" - // - // regular = "art-lojban" ; these tags match the 'langtag' - // / "cel-gaulish" ; production, but their subtags - // / "no-bok" ; are not extended language - // / "no-nyn" ; or variant subtags: their meaning - // / "zh-guoyu" ; is defined by their registration - // / "zh-hakka" ; and all of these are deprecated - // / "zh-min" ; in favor of a more modern - // / "zh-min-nan" ; subtag or sequence of subtags - // / "zh-xiang" - - final String[][] entries = { - //{"tag", "preferred"}, - {"art-lojban", "jbo"}, - {"cel-gaulish", "xtg-x-cel-gaulish"}, // fallback - {"en-GB-oed", "en-GB-x-oed"}, // fallback - {"i-ami", "ami"}, - {"i-bnn", "bnn"}, - {"i-default", "en-x-i-default"}, // fallback - {"i-enochian", "und-x-i-enochian"}, // fallback - {"i-hak", "hak"}, - {"i-klingon", "tlh"}, - {"i-lux", "lb"}, - {"i-mingo", "see-x-i-mingo"}, // fallback - {"i-navajo", "nv"}, - {"i-pwn", "pwn"}, - {"i-tao", "tao"}, - {"i-tay", "tay"}, - {"i-tsu", "tsu"}, - {"no-bok", "nb"}, - {"no-nyn", "nn"}, - {"sgn-BE-FR", "sfb"}, - {"sgn-BE-NL", "vgt"}, - {"sgn-CH-DE", "sgg"}, - {"zh-guoyu", "cmn"}, - {"zh-hakka", "hak"}, - {"zh-min", "nan-x-zh-min"}, // fallback - {"zh-min-nan", "nan"}, - {"zh-xiang", "hsn"}, - }; - LEGACY = HashMap.newHashMap(entries.length); - for (String[] e : entries) { - LEGACY.put(LocaleUtils.toLowerString(e[0]), e); - } - } - /* * BNF in RFC5646 * @@ -175,14 +107,10 @@ public record LanguageTag(String language, String script, String region, String StringTokenIterator itr; var errorMsg = new StringBuilder(); - // Check if the tag is a legacy language tag - String[] gfmap = LEGACY.get(LocaleUtils.toLowerString(languageTag)); - if (gfmap != null) { - // use preferred mapping - itr = new StringTokenIterator(gfmap[1], SEP); - } else { - itr = new StringTokenIterator(languageTag, SEP); - } + // Check if the tag is a legacy tag + var pref = legacyToPreferred(LocaleUtils.toLowerString(languageTag)); + // If legacy use preferred mapping, otherwise use the tag as is + itr = new StringTokenIterator(Objects.requireNonNullElse(pref, languageTag), SEP); String language = parseLanguage(itr, pp); List extlangs; @@ -400,15 +328,24 @@ public record LanguageTag(String language, String script, String region, String public static String caseFoldTag(String tag) { parse(tag, new ParsePosition(0), false); + StringBuilder bldr = new StringBuilder(tag.length()); + String[] subtags = tag.split(SEP); // Legacy tags - String potentialLegacy = tag.toLowerCase(Locale.ROOT); - if (LEGACY.containsKey(potentialLegacy)) { - return LEGACY.get(potentialLegacy)[0]; + if (legacyToPreferred(tag.toLowerCase(Locale.ROOT)) != null) { + // Fold the legacy tag + for (int i = 0; i < subtags.length ; i++) { + // 2 ALPHA Region subtag(s) are upper, all other subtags are lower + if (i > 0 && subtags[i].length() == 2) { + bldr.append(LocaleUtils.toUpperString(subtags[i])).append(SEP); + } else { + bldr.append(LocaleUtils.toLowerString(subtags[i])).append(SEP); + } + } + bldr.setLength(bldr.length() - 1); // Remove trailing '-' + return bldr.toString(); } // Non-legacy tags - StringBuilder bldr = new StringBuilder(tag.length()); - String[] subtags = tag.split("-"); boolean privateFound = false; boolean singletonFound = false; boolean privUseVarFound = false; @@ -435,7 +372,7 @@ public record LanguageTag(String language, String script, String region, String bldr.append(subtag.toLowerCase(Locale.ROOT)); } if (i != subtags.length-1) { - bldr.append("-"); + bldr.append(SEP); } } return bldr.substring(0); @@ -567,6 +504,47 @@ public record LanguageTag(String language, String script, String region, String return new LanguageTag(language, script, region, privateuse, EMPTY_SUBTAGS, variants, extensions); } + /* + * Converts a legacy tag to its preferred mapping if it exists, otherwise null. + * The keys are mapped and stored as lower case. (Folded on demand). + * See http://www.ietf.org/rfc/rfc5646.txt Section 2.1 and 2.2.8 for the + * full syntax and case accurate legacy tags. + */ + private static String legacyToPreferred(String tag) { + if (tag.length() < 5) { + return null; + } + return switch (tag) { + case "art-lojban" -> "jbo"; + case "cel-gaulish" -> "xtg-x-cel-gaulish"; // fallback + case "en-gb-oed" -> "en-GB-x-oed"; // fallback + case "i-ami" -> "ami"; + case "i-bnn" -> "bnn"; + case "i-default" -> "en-x-i-default"; // fallback + case "i-enochian" -> "und-x-i-enochian"; // fallback + case "i-hak", + "zh-hakka" -> "hak"; + case "i-klingon" -> "tlh"; + case "i-lux" -> "lb"; + case "i-mingo" -> "see-x-i-mingo"; // fallback + case "i-navajo" -> "nv"; + case "i-pwn" -> "pwn"; + case "i-tao" -> "tao"; + case "i-tay" -> "tay"; + case "i-tsu" -> "tsu"; + case "no-bok" -> "nb"; + case "no-nyn" -> "nn"; + case "sgn-be-fr" -> "sfb"; + case "sgn-be-nl" -> "vgt"; + case "sgn-ch-de" -> "sgg"; + case "zh-guoyu" -> "cmn"; + case "zh-min" -> "nan-x-zh-min"; // fallback + case "zh-min-nan" -> "nan"; + case "zh-xiang" -> "hsn"; + default -> null; + }; + } + // // Language subtag syntax checking methods // diff --git a/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java b/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java index 634932e9e19..7f66febf65f 100644 --- a/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java +++ b/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security index 5d96d74539e..2464361b9ef 100644 --- a/src/java.base/share/conf/security/java.security +++ b/src/java.base/share/conf/security/java.security @@ -771,6 +771,54 @@ jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, DTLSv1.0, RC4, DES, \ ECDH, TLS_RSA_*, rsa_pkcs1_sha1 usage HandshakeSignature, \ ecdsa_sha1 usage HandshakeSignature, dsa_sha1 usage HandshakeSignature +# +# Algorithm restrictions for Java Crypto API services +# +# In some environments, certain algorithms may be undesirable for certain +# cryptographic services. For example, "MD2" is generally no longer considered +# to be a secure hash algorithm. This section describes the mechanism for +# disabling algorithms at the JCA/JCE level based on service name and algorithm +# name. +# +# If a system property of the same name is also specified, it supersedes the +# security property value defined here. +# +# The syntax of the disabled services string is described as follows: +# "DisabledService {, DisabledService}" +# +# DisabledService: +# Service.AlgorithmName +# +# Service: (one of the following, more services may be added later) +# Cipher | KeyStore | MessageDigest | Signature +# +# AlgorithmName: +# (see below) +# +# The "AlgorithmName" is the standard algorithm name of the disabled +# service. See the Java Security Standard Algorithm Names Specification +# for information about Standard Algorithm Names. Matching is +# performed using a case-insensitive exact matching rule. For Cipher service, +# its algorithm is the transformation string. +# +# Note: If the property value contains entries with invalid syntax or +# unsupported services at the time of checking, an ExceptionInInitializerError +# with a cause of IllegalArgumentException will be thrown. +# +# Note: The restriction is applied in the various getInstance(...) methods +# of the supported Service classes, i.e. Cipher, KeyStore, MessageDigest, +# and Signature. If the algorithm is disabled, a NoSuchAlgorithmException will +# be thrown by the getInstance methods of Cipher, MessageDigest, and Signature +# and a KeyStoreException by the getInstance methods of KeyStore. +# +# Note: This property is currently used by the JDK Reference implementation. +# It is not guaranteed to be examined and used by other implementations. +# +# Example: +# jdk.crypto.disabledAlgorithms=Cipher.RSA/ECB/PKCS1Padding, MessageDigest.MD2 +# +#jdk.crypto.disabledAlgorithms= + # # Legacy algorithms for Secure Socket Layer/Transport Layer Security (SSL/TLS) # processing in JSSE implementation. @@ -923,6 +971,33 @@ jdk.tls.legacyAlgorithms=NULL, anon, RC4, DES, 3DES_EDE_CBC jdk.tls.keyLimits=AES/GCM/NoPadding KeyUpdate 2^37, \ ChaCha20-Poly1305 KeyUpdate 2^37 +# +# QUIC TLS key limits on symmetric cryptographic algorithms +# +# This security property sets limits on algorithms key usage in QUIC. +# When the number of encrypted datagrams reaches the algorithm value +# listed below, key update operation will be initiated. +# +# The syntax for the property is described below: +# KeyLimits: +# " KeyLimit { , KeyLimit } " +# +# KeyLimit: +# AlgorithmName Length +# +# AlgorithmName: +# A full algorithm transformation. +# +# Length: +# The amount of encrypted data in a session before the Action occurs +# This value may be an integer value in bytes, or as a power of two, 2^23. +# +# Note: This property is currently used by OpenJDK's JSSE implementation. It +# is not guaranteed to be examined and used by other implementations. +# +jdk.quic.tls.keyLimits=AES/GCM/NoPadding 2^23, \ + ChaCha20-Poly1305 2^23 + # # Cryptographic Jurisdiction Policy defaults # diff --git a/src/java.base/share/data/currency/CurrencyData.properties b/src/java.base/share/data/currency/CurrencyData.properties index ff2d1f87ed3..9b82318776a 100644 --- a/src/java.base/share/data/currency/CurrencyData.properties +++ b/src/java.base/share/data/currency/CurrencyData.properties @@ -32,7 +32,7 @@ formatVersion=3 # Version of the currency code information in this class. # It is a serial number that accompanies with each amendment. -dataVersion=179 +dataVersion=180 # List of all valid ISO 4217 currency codes. # To ensure compatibility, do not remove codes. @@ -147,7 +147,7 @@ IO=USD # BRUNEI DARUSSALAM BN=BND # BULGARIA -BG=BGN +BG=BGN;2025-12-31-22-00-00;EUR # BURKINA FASO BF=XOF # BURUNDI @@ -193,7 +193,7 @@ HR=EUR # CUBA CU=CUP # Curaçao -CW=ANG;2025-04-01-04-00-00;XCG +CW=XCG # CYPRUS CY=EUR # CZECHIA @@ -510,7 +510,7 @@ SR=SRD # SVALBARD AND JAN MAYEN SJ=NOK # Sint Maarten (Dutch part) -SX=ANG;2025-04-01-04-00-00;XCG +SX=XCG # ESWATINI SZ=SZL # SWEDEN diff --git a/src/java.base/share/man/java.md b/src/java.base/share/man/java.md index 1a6a944594f..d628cfee817 100644 --- a/src/java.base/share/man/java.md +++ b/src/java.base/share/man/java.md @@ -2448,7 +2448,7 @@ Java HotSpot VM. : Sets the initial amount of memory that the JVM will use for the Java heap before applying ergonomics heuristics as a percentage of the maximum amount determined as described in the `-XX:MaxRAM` option. The default value is - 1.5625 percent. + 0.2 percent. The following example shows how to set the percentage of the initial amount of memory used for the Java heap: diff --git a/src/java.base/share/native/libjava/Class.c b/src/java.base/share/native/libjava/Class.c index 9fb348d9217..95e8e2c169b 100644 --- a/src/java.base/share/native/libjava/Class.c +++ b/src/java.base/share/native/libjava/Class.c @@ -95,7 +95,7 @@ Java_java_lang_Class_registerNatives(JNIEnv *env, jclass cls) JNIEXPORT jclass JNICALL Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname, - jboolean initialize, jobject loader, jclass caller) + jboolean initialize, jobject loader) { char *clname; jclass cls = 0; @@ -133,7 +133,7 @@ Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname, goto done; } - cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller); + cls = JVM_FindClassFromLoader(env, clname, initialize, loader); done: if (clname != buf) { diff --git a/src/java.base/unix/classes/sun/nio/fs/UnixFileSystem.java b/src/java.base/unix/classes/sun/nio/fs/UnixFileSystem.java index 7a2bd71388d..8a2c2bb27e8 100644 --- a/src/java.base/unix/classes/sun/nio/fs/UnixFileSystem.java +++ b/src/java.base/unix/classes/sun/nio/fs/UnixFileSystem.java @@ -337,20 +337,6 @@ abstract class UnixFileSystem return Pattern.compile(expr); } - // Override if the platform uses different Unicode normalization form - // for native file path. For example on MacOSX, the native path is stored - // in Unicode NFD form. - String normalizeNativePath(String path) { - return path; - } - - // Override if the native file path use non-NFC form. For example on MacOSX, - // the native path is stored in Unicode NFD form, the path need to be - // normalized back to NFC before passed back to Java level. - String normalizeJavaPath(String path) { - return path; - } - // Unix implementation of Files#copy and Files#move methods. // calculate the least common multiple of two values; diff --git a/src/java.base/unix/classes/sun/nio/fs/UnixPath.java b/src/java.base/unix/classes/sun/nio/fs/UnixPath.java index 5a77bb0b935..b722c30db42 100644 --- a/src/java.base/unix/classes/sun/nio/fs/UnixPath.java +++ b/src/java.base/unix/classes/sun/nio/fs/UnixPath.java @@ -42,6 +42,7 @@ import java.util.Objects; import jdk.internal.access.JavaLangAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.ArraysSupport; +import jdk.internal.vm.annotation.Stable; import static sun.nio.fs.UnixConstants.*; import static sun.nio.fs.UnixNativeDispatcher.*; @@ -59,7 +60,7 @@ class UnixPath implements Path { private final byte[] path; // String representation (created lazily, no need to be volatile) - private String stringValue; + private @Stable String stringValue; // cached hashcode (created lazily, no need to be volatile) private int hash; @@ -124,7 +125,6 @@ class UnixPath implements Path { // encodes the given path-string into a sequence of bytes private static byte[] encode(UnixFileSystem fs, String input) { - input = fs.normalizeNativePath(input); try { return JLA.uncheckedGetBytesOrThrow(input, Util.jnuEncoding()); } catch (CharacterCodingException cce) { @@ -814,7 +814,7 @@ class UnixPath implements Path { // OK if two or more threads create a String String stringValue = this.stringValue; if (stringValue == null) { - this.stringValue = stringValue = fs.normalizeJavaPath(Util.toString(path)); // platform encoding + this.stringValue = stringValue = Util.toString(path); // platform encoding } return stringValue; } @@ -948,28 +948,51 @@ class UnixPath implements Path { // Obtain the stream of entries in the directory corresponding // to the path constructed thus far, and extract the entry whose - // key is equal to the key of the current element + // internal path bytes equal the internal path bytes of the current + // element, or whose key is equal to the key of the current element + boolean found = false; DirectoryStream.Filter filter = (p) -> { return true; }; + // compare path bytes until a match is found or no more entries try (DirectoryStream entries = new UnixDirectoryStream(path, dp, filter)) { - boolean found = false; for (Path entry : entries) { - UnixPath p = path.resolve(entry.getFileName()); - UnixFileAttributes attributes = null; - try { - attributes = UnixFileAttributes.get(p, false); - UnixFileKey key = attributes.fileKey(); - if (key.equals(elementKey)) { - path = path.resolve(entry); - found = true; - break; + Path name = entry.getFileName(); + if (name.compareTo(element) == 0) { + found = true; + path = path.resolve(entry); + break; + } + } + } + + // if no path match found, compare file keys + if (!found) { + try { + dp = opendir(path); + } catch (UnixException x) { + x.rethrowAsIOException(path); + } + + try (DirectoryStream entries = new UnixDirectoryStream(path, dp, filter)) { + for (Path entry : entries) { + Path name = entry.getFileName(); + UnixPath p = path.resolve(name); + UnixFileAttributes attributes = null; + try { + attributes = UnixFileAttributes.get(p, false); + UnixFileKey key = attributes.fileKey(); + if (key.equals(elementKey)) { + found = true; + path = path.resolve(entry); + break; + } + } catch (UnixException ignore) { + continue; } - } catch (UnixException ignore) { - continue; } } - // Fallback which should in theory never happen if (!found) { + // Fallback which should in theory never happen path = path.resolve(element); } } diff --git a/src/java.base/unix/native/libjava/childproc.c b/src/java.base/unix/native/libjava/childproc.c index c4b5a2d7b29..9c6334e52d2 100644 --- a/src/java.base/unix/native/libjava/childproc.c +++ b/src/java.base/unix/native/libjava/childproc.c @@ -67,24 +67,39 @@ markCloseOnExec(int fd) return 0; } +#if !defined(_AIX) + /* The /proc file system on AIX does not contain open system files + * like /dev/random. Therefore we use a different approach and do + * not need isAsciiDigit() or FD_DIR */ static int isAsciiDigit(char c) { return c >= '0' && c <= '9'; } -#if defined(_AIX) - /* AIX does not understand '/proc/self' - it requires the real process ID */ - #define FD_DIR aix_fd_dir -#elif defined(_ALLBSD_SOURCE) - #define FD_DIR "/dev/fd" -#else - #define FD_DIR "/proc/self/fd" + #if defined(_ALLBSD_SOURCE) + #define FD_DIR "/dev/fd" + #else + #define FD_DIR "/proc/self/fd" + #endif #endif static int markDescriptorsCloseOnExec(void) { +#if defined(_AIX) + /* On AIX, we cannot rely on proc file system iteration to find all open files. Since + * iteration over all possible file descriptors, and subsequently closing them, can + * take a very long time, we use a bulk close via `ioctl` that is available on AIX. + * Since we hard-close, we need to make sure to keep the fail pipe file descriptor + * alive until the exec call. Therefore we mark the fail pipe fd with close on exec + * like the other OSes do, but then proceed to hard-close file descriptors beyond that. + */ + if (fcntl(FAIL_FILENO + 1, F_CLOSEM, 0) == -1 || + (markCloseOnExec(FAIL_FILENO) == -1 && errno != EBADF)) { + return -1; + } +#else DIR *dp; struct dirent *dirp; /* This function marks all file descriptors beyond stderr as CLOEXEC. @@ -93,12 +108,6 @@ markDescriptorsCloseOnExec(void) * execve. */ const int fd_from = STDERR_FILENO + 1; -#if defined(_AIX) - /* AIX does not understand '/proc/self' - it requires the real process ID */ - char aix_fd_dir[32]; /* the pid has at most 19 digits */ - snprintf(aix_fd_dir, 32, "/proc/%d/fd", getpid()); -#endif - if ((dp = opendir(FD_DIR)) == NULL) return -1; @@ -114,6 +123,7 @@ markDescriptorsCloseOnExec(void) } closedir(dp); +#endif return 0; } @@ -406,6 +416,10 @@ childProcess(void *arg) /* We moved the fail pipe fd */ fail_pipe_fd = FAIL_FILENO; + /* For AIX: The code in markDescriptorsCloseOnExec() relies on the current + * semantic of this function. When this point here is reached only the + * FDs 0,1,2 and 3 are further used until the exec() or the exit(-1). */ + /* close everything */ if (markDescriptorsCloseOnExec() == -1) { /* failed, close the old way */ int max_fd = (int)sysconf(_SC_OPEN_MAX); diff --git a/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c b/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c index 60ccdfc45fc..4b5cfabebfb 100644 --- a/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c +++ b/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c @@ -55,6 +55,7 @@ #include #ifdef __linux__ +#include // For uintXX_t types used in statx support #include #include // makedev macros #endif @@ -70,19 +71,12 @@ // by defining binary compatible statx structs in this file and // not relying on included headers. -#ifndef __GLIBC__ -// Alpine doesn't know these types, define them -typedef unsigned int __uint32_t; -typedef unsigned short __uint16_t; -typedef unsigned long int __uint64_t; -#endif - /* * Timestamp structure for the timestamps in struct statx. */ struct my_statx_timestamp { int64_t tv_sec; - __uint32_t tv_nsec; + uint32_t tv_nsec; int32_t __reserved; }; @@ -92,27 +86,27 @@ struct my_statx_timestamp { */ struct my_statx { - __uint32_t stx_mask; - __uint32_t stx_blksize; - __uint64_t stx_attributes; - __uint32_t stx_nlink; - __uint32_t stx_uid; - __uint32_t stx_gid; - __uint16_t stx_mode; - __uint16_t __statx_pad1[1]; - __uint64_t stx_ino; - __uint64_t stx_size; - __uint64_t stx_blocks; - __uint64_t stx_attributes_mask; + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t __statx_pad1[1]; + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; struct my_statx_timestamp stx_atime; struct my_statx_timestamp stx_btime; struct my_statx_timestamp stx_ctime; struct my_statx_timestamp stx_mtime; - __uint32_t stx_rdev_major; - __uint32_t stx_rdev_minor; - __uint32_t stx_dev_major; - __uint32_t stx_dev_minor; - __uint64_t __statx_pad2[14]; + uint32_t stx_rdev_major; + uint32_t stx_rdev_minor; + uint32_t stx_dev_major; + uint32_t stx_dev_minor; + uint64_t __statx_pad2[14]; }; // statx masks, flags, constants diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java b/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java index b1de66ac4f2..0c09a80e99e 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java @@ -96,6 +96,7 @@ class WindowsConstants { public static final int ERROR_NOT_SAME_DEVICE = 17; public static final int ERROR_NOT_READY = 21; public static final int ERROR_SHARING_VIOLATION = 32; + public static final int ERROR_NETWORK_ACCESS_DENIED = 65; public static final int ERROR_FILE_EXISTS = 80; public static final int ERROR_INVALID_PARAMETER = 87; public static final int ERROR_DISK_FULL = 112; @@ -110,6 +111,7 @@ class WindowsConstants { public static final int ERROR_PRIVILEGE_NOT_HELD = 1314; public static final int ERROR_NONE_MAPPED = 1332; public static final int ERROR_CANT_ACCESS_FILE = 1920; + public static final int ERROR_CANT_RESOLVE_FILENAME = 1921; public static final int ERROR_NOT_A_REPARSE_POINT = 4390; public static final int ERROR_INVALID_REPARSE_DATA = 4392; diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsException.java b/src/java.base/windows/classes/sun/nio/fs/WindowsException.java index f1eff69210b..532728f57ed 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsException.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -76,20 +76,21 @@ class WindowsException extends Exception { } private IOException translateToIOException(String file, String other) { - // not created with last error - if (lastError() == 0) - return new IOException(errorString()); + return switch (lastError()) { + // not created with last error + case 0 -> new IOException(errorString()); - // handle specific cases - if (lastError() == ERROR_FILE_NOT_FOUND || lastError() == ERROR_PATH_NOT_FOUND) - return new NoSuchFileException(file, other, null); - if (lastError() == ERROR_FILE_EXISTS || lastError() == ERROR_ALREADY_EXISTS) - return new FileAlreadyExistsException(file, other, null); - if (lastError() == ERROR_ACCESS_DENIED) - return new AccessDeniedException(file, other, null); + // handle specific cases + case ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND + -> new NoSuchFileException(file, other, null); + case ERROR_FILE_EXISTS, ERROR_ALREADY_EXISTS + -> new FileAlreadyExistsException(file, other, null); + case ERROR_ACCESS_DENIED, ERROR_NETWORK_ACCESS_DENIED, ERROR_PRIVILEGE_NOT_HELD + -> new AccessDeniedException(file, other, null); - // fallback to the more general exception - return new FileSystemException(file, other, errorString()); + // fallback to the more general exception + default -> new FileSystemException(file, other, errorString()); + }; } void rethrowAsIOException(String file) throws IOException { diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java b/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java index 5e740ec1f4d..58e6294cc12 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java @@ -418,65 +418,178 @@ class WindowsFileSystemProvider } } + /** + * Contains the attributes of a given file system entry and the open + * handle from which they were obtained. The handle must remain open + * until the volume serial number and file index of the attributes + * are no longer needed for comparison with other attributes. + * + * @param attrs the file system entry attributes + * @param handle the open Windows file handle + */ + private record EntryAttributes(WindowsFileAttributes attrs, long handle) { + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof EntryAttributes other) { + WindowsFileAttributes oattrs = other.attrs(); + return oattrs.volSerialNumber() == attrs.volSerialNumber() && + oattrs.fileIndexHigh() == attrs.fileIndexHigh() && + oattrs.fileIndexLow() == attrs.fileIndexLow(); + } + return false; + } + + public int hashCode() { + return attrs.volSerialNumber() + + attrs.fileIndexHigh() + attrs.fileIndexLow(); + } + } + + /** + * Returns the attributes of the file located by the given path if it is a + * symbolic link. The handle contained in the returned value must be closed + * once the attributes are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if no link is found + */ + private EntryAttributes linkAttributes(WindowsPath path) + throws WindowsException + { + long h = INVALID_HANDLE_VALUE; + try { + h = path.openForReadAttributeAccess(false); + } catch (WindowsException x) { + if (x.lastError() != ERROR_FILE_NOT_FOUND && + x.lastError() != ERROR_PATH_NOT_FOUND) + throw x; + return null; + } + + WindowsFileAttributes attrs = null; + try { + attrs = WindowsFileAttributes.readAttributes(h); + } finally { + if (attrs == null || !attrs.isSymbolicLink()) { + CloseHandle(h); + return null; + } + } + + return new EntryAttributes(attrs, h); + } + + /** + * Returns the attributes of the last symbolic link encountered in the + * specified path. Links are not resolved in the path taken as a whole, + * but rather the first link is followed, then its target, and so on, + * until no more links are encountered. The handle contained in the + * returned value must be closed once the attributes are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if no links are found + * @throws FileSystemLoopException if a symbolic link cycle is encountered + */ + private EntryAttributes lastLinkAttributes(WindowsPath path) + throws IOException, WindowsException + { + var linkAttrs = new LinkedHashSet(); + try { + while (path != null) { + EntryAttributes linkAttr = linkAttributes(path); + if (linkAttr == null) + break; + + if (!linkAttrs.add(linkAttr)) { + // the element was not added to the set so close its handle + // here as it would not be closed in the finally block + CloseHandle(linkAttr.handle()); + throw new FileSystemLoopException(path.toString()); + } + + String target = WindowsLinkSupport.readLink(path, linkAttr.handle()); + path = WindowsPath.parse(path.getFileSystem(), target); + } + + if (!linkAttrs.isEmpty()) + return linkAttrs.removeLast(); + } finally { + linkAttrs.stream().forEach(la -> CloseHandle(la.handle())); + } + + return null; + } + + /** + * Returns the attributes of the file located by the supplied parameter + * with all symbolic links in its path resolved. If the file located by + * the resolved path does not exist, then null is returned. The handle + * contained in the returned value must be closed once the attributes + * are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if the real path does not exist + */ + private EntryAttributes realPathAttributes(WindowsPath path) + throws WindowsException + { + long h; + try { + h = path.openForReadAttributeAccess(true); + } catch (WindowsException x) { + if (x.lastError() == ERROR_FILE_NOT_FOUND || + x.lastError() == ERROR_PATH_NOT_FOUND || + x.lastError() == ERROR_CANT_RESOLVE_FILENAME) + return null; + + throw x; + } + + WindowsFileAttributes attrs = null; + try { + attrs = WindowsFileAttributes.readAttributes(h); + } catch (WindowsException x) { + CloseHandle(h); + throw x; + } + + return new EntryAttributes(attrs, h); + } + @Override public boolean isSameFile(Path obj1, Path obj2) throws IOException { + // toWindowsPath verifies its argument is a non-null WindowsPath WindowsPath file1 = WindowsPath.toWindowsPath(obj1); if (file1.equals(obj2)) return true; if (obj2 == null) throw new NullPointerException(); - if (!(obj2 instanceof WindowsPath)) + if (!(obj2 instanceof WindowsPath file2)) return false; - WindowsPath file2 = (WindowsPath)obj2; - // open both files and see if they are the same - long h1 = 0L; + EntryAttributes attrs1 = null; + EntryAttributes attrs2 = null; + WindowsPath pathForException = file1; try { - h1 = file1.openForReadAttributeAccess(true); + if ((attrs1 = realPathAttributes(file1)) != null || + (attrs1 = lastLinkAttributes(file1)) != null) { + pathForException = file2; + if ((attrs2 = realPathAttributes(file2)) != null || + (attrs2 = lastLinkAttributes(file2)) != null) + return attrs1.equals(attrs2); + } } catch (WindowsException x) { - if (x.lastError() != ERROR_FILE_NOT_FOUND && - x.lastError() != ERROR_PATH_NOT_FOUND) - x.rethrowAsIOException(file1); - } - - // if file1 does not exist, it cannot equal file2 - if (h1 == 0L) - return false; - - try { - WindowsFileAttributes attrs1 = null; - try { - attrs1 = WindowsFileAttributes.readAttributes(h1); - } catch (WindowsException x) { - x.rethrowAsIOException(file1); - } - long h2 = 0L; - try { - h2 = file2.openForReadAttributeAccess(true); - } catch (WindowsException x) { - if (x.lastError() != ERROR_FILE_NOT_FOUND && - x.lastError() != ERROR_PATH_NOT_FOUND) - x.rethrowAsIOException(file2); - } - - // if file2 does not exist, it cannot equal file1, which does - if (h2 == 0L) - return false; - - try { - WindowsFileAttributes attrs2 = null; - try { - attrs2 = WindowsFileAttributes.readAttributes(h2); - } catch (WindowsException x) { - x.rethrowAsIOException(file2); - } - return WindowsFileAttributes.isSameFile(attrs1, attrs2); - } finally { - CloseHandle(h2); - } + x.rethrowAsIOException(pathForException); } finally { - CloseHandle(h1); + if (attrs1 != null) { + CloseHandle(attrs1.handle()); + if (attrs2 != null) + CloseHandle(attrs2.handle()); + } } + + return false; } @Override diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java b/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java index a5adfb73ebc..fc79153914d 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java @@ -84,7 +84,7 @@ class WindowsLinkSupport { x.rethrowAsIOException(path); } try { - return readLinkImpl(path, handle); + return readLink(path, handle); } finally { CloseHandle(handle); } @@ -289,9 +289,7 @@ class WindowsLinkSupport { * Returns target of a symbolic link given the handle of an open file * (that should be a link). */ - private static String readLinkImpl(WindowsPath path, long handle) - throws IOException - { + static String readLink(WindowsPath path, long handle) throws IOException { int size = MAXIMUM_REPARSE_DATA_BUFFER_SIZE; try (NativeBuffer buffer = NativeBuffers.getNativeBuffer(size)) { try { diff --git a/src/java.base/windows/native/libjava/canonicalize_md.c b/src/java.base/windows/native/libjava/canonicalize_md.c index ecfdf63d091..7e567c7fbb4 100644 --- a/src/java.base/windows/native/libjava/canonicalize_md.c +++ b/src/java.base/windows/native/libjava/canonicalize_md.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,7 +41,7 @@ /* We should also include jdk_util.h here, for the prototype of JDK_Canonicalize. This isn't possible though because canonicalize_md.c is as well used in different contexts within Oracle. - */ +*/ #include "io_util_md.h" /* Copy bytes to dst, not going past dend; return dst + number of bytes copied, @@ -139,7 +139,8 @@ lastErrorReportable() || (errval == ERROR_ACCESS_DENIED) || (errval == ERROR_NETWORK_UNREACHABLE) || (errval == ERROR_NETWORK_ACCESS_DENIED) - || (errval == ERROR_NO_MORE_FILES)) { + || (errval == ERROR_NO_MORE_FILES) + || (errval == ERROR_NETNAME_DELETED)) { return 0; } return 1; @@ -183,7 +184,7 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) /* Copy prefix, assuming path is absolute */ c = src[0]; if (((c <= L'z' && c >= L'a') || (c <= L'Z' && c >= L'A')) - && (src[1] == L':') && (src[2] == L'\\')) { + && (src[1] == L':') && (src[2] == L'\\')) { /* Drive specifier */ *src = towupper(*src); /* Canonicalize drive letter */ if (!(dst = wcp(dst, dend, L'\0', src, src + 2))) { @@ -244,9 +245,9 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) continue; } else { if (!lastErrorReportable()) { - if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))){ - goto err; - } + if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))){ + goto err; + } break; } else { goto err; @@ -255,7 +256,7 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) } if (dst >= dend) { - errno = ENAMETOOLONG; + errno = ENAMETOOLONG; goto err; } *dst = L'\0'; @@ -366,7 +367,7 @@ JDK_Canonicalize(const char *orig, char *out, int len) { // Change return value to success. ret = 0; -finish: + finish: free(wresult); free(wpath); diff --git a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java index c3ed089472b..45fcdfd2c0c 100644 --- a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java +++ b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -75,4 +75,16 @@ public final class CPrinterGraphics extends ProxyGraphics2D { // needToCopyBgColorImage, is private instead of protected!) return getDelegate().drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); } + + @Override + public void drawString(String str, int x, int y) { + str = RasterPrinterJob.removeControlChars(str); + super.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + str = RasterPrinterJob.removeControlChars(str); + super.drawString(str, x, y); + } } diff --git a/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m b/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m index 9da3b6648fb..f1c12585728 100644 --- a/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m +++ b/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -457,15 +457,17 @@ static inline void doDrawGlyphsPipe_fillGlyphAndAdvanceBuffers } } if (positions != NULL) { + + CGAffineTransform invTx = CGAffineTransformInvert(strike->fFontTx); + CGPoint prev; prev.x = positions[0]; prev.y = positions[1]; + prev = CGPointApplyAffineTransform(prev, invTx); // take the first point, and move the context to that location CGContextTranslateCTM(qsdo->cgRef, prev.x, prev.y); - CGAffineTransform invTx = CGAffineTransformInvert(strike->fFontTx); - // for each position, figure out the advance (since CG won't take positions directly) size_t i; for (i = 0; i < length - 1; i++) @@ -476,7 +478,7 @@ static inline void doDrawGlyphsPipe_fillGlyphAndAdvanceBuffers pt.y = positions[i2+1]; pt = CGPointApplyAffineTransform(pt, invTx); advances[i].width = pt.x - prev.x; - advances[i].height = -(pt.y - prev.y); // negative to translate to device space + advances[i].height = pt.y - prev.y; prev.x = pt.x; prev.y = pt.y; } diff --git a/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java b/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java index 91e0f8dc54d..552ef870dbe 100644 --- a/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java +++ b/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java @@ -146,11 +146,15 @@ public class SwingUtilities3 { } public static void applyInsets(Rectangle rect, Insets insets) { + applyInsets(rect, insets, true); + } + + public static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) { if (insets != null) { - rect.x += insets.left; + rect.x += leftToRight ? insets.left : insets.right; rect.y += insets.top; - rect.width -= (insets.right + rect.x); - rect.height -= (insets.bottom + rect.y); + rect.width -= (insets.left + insets.right); + rect.height -= (insets.top + insets.bottom); } } diff --git a/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java b/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java index 8bf09195627..0c2902ba74a 100644 --- a/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java +++ b/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java @@ -41,6 +41,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; @@ -1549,33 +1550,19 @@ public sealed class ICC_Profile implements Serializable private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); - - String csName = (String) s.readObject(); - byte[] data = (byte[]) s.readObject(); - - int cspace = 0; // ColorSpace.CS_* constant if known - boolean isKnownPredefinedCS = false; - if (csName != null) { - isKnownPredefinedCS = true; - if (csName.equals("CS_sRGB")) { - cspace = ColorSpace.CS_sRGB; - } else if (csName.equals("CS_CIEXYZ")) { - cspace = ColorSpace.CS_CIEXYZ; - } else if (csName.equals("CS_PYCC")) { - cspace = ColorSpace.CS_PYCC; - } else if (csName.equals("CS_GRAY")) { - cspace = ColorSpace.CS_GRAY; - } else if (csName.equals("CS_LINEAR_RGB")) { - cspace = ColorSpace.CS_LINEAR_RGB; - } else { - isKnownPredefinedCS = false; - } - } - - if (isKnownPredefinedCS) { - resolvedDeserializedProfile = getInstance(cspace); - } else { - resolvedDeserializedProfile = getInstance(data); + try { + String csName = (String) s.readObject(); + byte[] data = (byte[]) s.readObject(); + resolvedDeserializedProfile = switch (csName) { + case "CS_sRGB" -> getInstance(ColorSpace.CS_sRGB); + case "CS_CIEXYZ" -> getInstance(ColorSpace.CS_CIEXYZ); + case "CS_PYCC" -> getInstance(ColorSpace.CS_PYCC); + case "CS_GRAY" -> getInstance(ColorSpace.CS_GRAY); + case "CS_LINEAR_RGB" -> getInstance(ColorSpace.CS_LINEAR_RGB); + case null, default -> getInstance(data); + }; + } catch (ClassCastException | IllegalArgumentException e) { + throw new InvalidObjectException("Invalid ICC Profile Data", e); } } diff --git a/src/java.desktop/share/classes/java/awt/font/TextLayout.java b/src/java.desktop/share/classes/java/awt/font/TextLayout.java index 74382b4bd83..5e8f3040055 100644 --- a/src/java.desktop/share/classes/java/awt/font/TextLayout.java +++ b/src/java.desktop/share/classes/java/awt/font/TextLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -256,7 +256,6 @@ public final class TextLayout implements Cloneable { */ private boolean cacheIsValid = false; - // This value is obtained from an attribute, and constrained to the // interval [0,1]. If 0, the layout cannot be justified. private float justifyRatio; @@ -367,6 +366,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(String string, Font font, FontRenderContext frc) { @@ -378,8 +378,8 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null string passed to TextLayout constructor."); } - if (string.length() == 0) { - throw new IllegalArgumentException("Zero length string passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } Map attributes = null; @@ -415,6 +415,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(String string, Map attributes, FontRenderContext frc) @@ -427,8 +428,8 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null map passed to TextLayout constructor."); } - if (string.length() == 0) { - throw new IllegalArgumentException("Zero length string passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } char[] text = string.toCharArray(); @@ -499,6 +500,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(AttributedCharacterIterator text, FontRenderContext frc) { @@ -506,14 +508,13 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null iterator passed to TextLayout constructor."); } - int start = text.getBeginIndex(); - int limit = text.getEndIndex(); - if (start == limit) { - throw new IllegalArgumentException("Zero length iterator passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } + int start = text.getBeginIndex(); + int limit = text.getEndIndex(); int len = limit - start; - text.first(); char[] chars = new char[len]; int n = 0; for (char c = text.first(); @@ -1125,7 +1126,12 @@ public final class TextLayout implements Cloneable { float top1X, top2X; float bottom1X, bottom2X; - if (caret == 0 || caret == characterCount) { + if (caret == 0 && characterCount == 0) { + + top1X = top2X = 0; + bottom1X = bottom2X = 0; + + } else if (caret == 0 || caret == characterCount) { float pos; int logIndex; @@ -1143,8 +1149,8 @@ public final class TextLayout implements Cloneable { pos += angle * shift; top1X = top2X = pos + angle*textLine.getCharAscent(logIndex); bottom1X = bottom2X = pos - angle*textLine.getCharDescent(logIndex); - } - else { + + } else { { int logIndex = textLine.visualToLogical(caret-1); @@ -1884,7 +1890,6 @@ public final class TextLayout implements Cloneable { Shape[] result = new Shape[2]; TextHitInfo hit = TextHitInfo.afterOffset(offset); - int hitCaret = hitToCaret(hit); LayoutPathImpl lp = textLine.getLayoutPath(); @@ -2180,12 +2185,16 @@ public final class TextLayout implements Cloneable { checkTextHit(firstEndpoint); checkTextHit(secondEndpoint); - if(bounds == null) { - throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()"); + if (bounds == null) { + throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()"); } GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + if (characterCount == 0) { + return result; + } + int firstCaret = hitToCaret(firstEndpoint); int secondCaret = hitToCaret(secondEndpoint); @@ -2194,8 +2203,9 @@ public final class TextLayout implements Cloneable { if (firstCaret == 0 || secondCaret == 0) { GeneralPath ls = leftShape(bounds); - if (!ls.getBounds().isEmpty()) + if (!ls.getBounds().isEmpty()) { result.append(ls, false); + } } if (firstCaret == characterCount || secondCaret == characterCount) { @@ -2282,12 +2292,16 @@ public final class TextLayout implements Cloneable { secondEndpoint = t; } - if(firstEndpoint < 0 || secondEndpoint > characterCount) { + if (firstEndpoint < 0 || secondEndpoint > characterCount) { throw new IllegalArgumentException("Range is invalid in TextLayout.getLogicalHighlightShape()"); } GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + if (characterCount == 0) { + return result; + } + int[] carets = new int[10]; // would this ever not handle all cases? int count = 0; diff --git a/src/java.desktop/share/classes/java/awt/font/TextLine.java b/src/java.desktop/share/classes/java/awt/font/TextLine.java index 681fcd90083..3464c1626c6 100644 --- a/src/java.desktop/share/classes/java/awt/font/TextLine.java +++ b/src/java.desktop/share/classes/java/awt/font/TextLine.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -836,7 +836,7 @@ final class TextLine { } if (result == null) { - result = new Rectangle2D.Float(Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + result = new Rectangle2D.Float(0, 0, 0, 0); } return result; @@ -844,6 +844,10 @@ final class TextLine { public Rectangle2D getItalicBounds() { + if (fComponents.length == 0) { + return new Rectangle2D.Float(0, 0, 0, 0); + } + float left = Float.MAX_VALUE, right = -Float.MAX_VALUE; float top = Float.MAX_VALUE, bottom = -Float.MAX_VALUE; @@ -927,7 +931,7 @@ final class TextLine { // dlf: get baseRot from font for now??? if (!requiresBidi) { - requiresBidi = Bidi.requiresBidi(chars, 0, chars.length); + requiresBidi = Bidi.requiresBidi(chars, 0, characterCount); } if (requiresBidi) { @@ -935,7 +939,7 @@ final class TextLine { ? Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT : values.getRunDirection(); - bidi = new Bidi(chars, 0, embs, 0, chars.length, bidiflags); + bidi = new Bidi(chars, 0, embs, 0, characterCount, bidiflags); if (!bidi.isLeftToRight()) { levels = BidiUtils.getLevels(bidi); int[] charsVtoL = BidiUtils.createVisualToLogicalMap(levels); @@ -945,13 +949,11 @@ final class TextLine { } Decoration decorator = Decoration.getDecoration(values); - int layoutFlags = 0; // no extra info yet, bidi determines run and line direction TextLabelFactory factory = new TextLabelFactory(frc, chars, bidi, layoutFlags); TextLineComponent[] components = new TextLineComponent[1]; - - components = createComponentsOnRun(0, chars.length, + components = createComponentsOnRun(0, characterCount, chars, charsLtoV, levels, factory, font, lm, @@ -972,7 +974,7 @@ final class TextLine { } return new TextLine(frc, components, lm.baselineOffsets, - chars, 0, chars.length, charsLtoV, levels, isDirectionLTR); + chars, 0, characterCount, charsLtoV, levels, isDirectionLTR); } private static TextLineComponent[] expandArray(TextLineComponent[] orig) { diff --git a/src/java.desktop/share/classes/java/awt/image/LookupOp.java b/src/java.desktop/share/classes/java/awt/image/LookupOp.java index 5d11f78e76d..c8d8a703a3f 100644 --- a/src/java.desktop/share/classes/java/awt/image/LookupOp.java +++ b/src/java.desktop/share/classes/java/awt/image/LookupOp.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/java.desktop/share/classes/java/beans/Beans.java b/src/java.desktop/share/classes/java/beans/Beans.java index 313bfe98515..a95aeb45cbb 100644 --- a/src/java.desktop/share/classes/java/beans/Beans.java +++ b/src/java.desktop/share/classes/java/beans/Beans.java @@ -64,6 +64,22 @@ public class Beans { *

    * Instantiate a JavaBean. *

    + * The bean is created based on a name relative to a class-loader. + * This name should be a {@linkplain ClassLoader##binary-name binary name} of a class such as "a.b.C". + *

    + * The given name can indicate either a serialized object or a class. + * We first try to treat the {@code beanName} as a serialized object + * name then as a class name. + *

    + * When using the {@code beanName} as a serialized object name we convert the + * given {@code beanName} to a resource pathname and add a trailing ".ser" suffix. + * We then try to load a serialized object from that resource. + *

    + * For example, given a {@code beanName} of "x.y", {@code Beans.instantiate} would first + * try to read a serialized object from the resource "x/y.ser" and if + * that failed it would try to load the class "x.y" and create an + * instance of that class. + * * @return a JavaBean * @param cls the class-loader from which we should create * the bean. If this is null, then the system @@ -84,6 +100,22 @@ public class Beans { *

    * Instantiate a JavaBean. *

    + * The bean is created based on a name relative to a class-loader. + * This name should be a {@linkplain ClassLoader##binary-name binary name} of a class such as "a.b.C". + *

    + * The given name can indicate either a serialized object or a class. + * We first try to treat the {@code beanName} as a serialized object + * name then as a class name. + *

    + * When using the {@code beanName} as a serialized object name we convert the + * given {@code beanName} to a resource pathname and add a trailing ".ser" suffix. + * We then try to load a serialized object from that resource. + *

    + * For example, given a {@code beanName} of "x.y", {@code Beans.instantiate} would first + * try to read a serialized object from the resource "x/y.ser" and if + * that failed it would try to load the class "x.y" and create an + * instance of that class. + * * @return a JavaBean * * @param cls the class-loader from which we should create diff --git a/src/java.desktop/share/classes/javax/swing/JSplitPane.java b/src/java.desktop/share/classes/javax/swing/JSplitPane.java index f21b2b3339a..86a1a2495c7 100644 --- a/src/java.desktop/share/classes/javax/swing/JSplitPane.java +++ b/src/java.desktop/share/classes/javax/swing/JSplitPane.java @@ -371,24 +371,31 @@ public class JSplitPane extends JComponent implements Accessible public void setComponentOrientation(ComponentOrientation orientation) { ComponentOrientation curOrn = this.getComponentOrientation(); super.setComponentOrientation(orientation); + Component comp = null; if (!orientation.equals(curOrn)) { Component leftComponent = this.getLeftComponent(); Component rightComponent = this.getRightComponent(); if (!this.getComponentOrientation().isLeftToRight()) { if (rightComponent != null) { - setLeftComponent(rightComponent); - } - if (leftComponent != null) { - setRightComponent(leftComponent); + comp = this.leftComponent; + this.leftComponent = this.rightComponent; + this.rightComponent = comp; + } else if (leftComponent != null) { + comp = this.rightComponent; + this.rightComponent = this.leftComponent; + this.leftComponent = comp; } } else { if (leftComponent != null) { - setLeftComponent(leftComponent); + this.leftComponent = rightComponent; } if (rightComponent != null) { - setRightComponent(rightComponent); + this.rightComponent = leftComponent; } } + firePropertyChange(ORIENTATION_PROPERTY, curOrn, orientation); + this.revalidate(); + this.repaint(); } } diff --git a/src/java.desktop/share/classes/javax/swing/SwingUtilities.java b/src/java.desktop/share/classes/javax/swing/SwingUtilities.java index ebf39ac0283..4633e9c4756 100644 --- a/src/java.desktop/share/classes/javax/swing/SwingUtilities.java +++ b/src/java.desktop/share/classes/javax/swing/SwingUtilities.java @@ -49,10 +49,6 @@ import sun.awt.AWTAccessor.MouseEventAccessor; */ public class SwingUtilities implements SwingConstants { - // These states are system-wide, rather than AppContext wide. - private static boolean canAccessEventQueue = false; - private static boolean eventQueueTested = false; - /** * Indicates if we should change the drop target when a * {@code TransferHandler} is set. diff --git a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java index d361906b291..348d58bab21 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java @@ -682,7 +682,7 @@ public class BasicMenuItemUI extends MenuItemUI g.setFont(mi.getFont()); Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); - applyInsets(viewRect, mi.getInsets()); + SwingUtilities3.applyInsets(viewRect, mi.getInsets()); MenuItemLayoutHelper lh = new MenuItemLayoutHelper(mi, checkIcon, arrowIcon, viewRect, defaultTextIconGap, acceleratorDelimiter, @@ -741,10 +741,6 @@ public class BasicMenuItemUI extends MenuItemUI SwingUtilities3.paintArrowIcon(g, lh, lr, foreground); } - private void applyInsets(Rectangle rect, Insets insets) { - SwingUtilities3.applyInsets(rect, insets); - } - /** * Draws the background of the menu item. * diff --git a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java index e518f509c5a..6fab795e36c 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -377,6 +377,7 @@ public class BasicPopupMenuUI extends PopupMenuUI { } else if (item.isEnabled()) { // we have a menu item manager.clearSelectedPath(); + sun.awt.SunToolkit.consumeNextKeyTyped(e); item.doClick(); } e.consume(); diff --git a/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java b/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java index 41d90d31dd4..7b9a23aec5d 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -2291,8 +2291,11 @@ public class MetalLookAndFeel extends BasicLookAndFeel setUpdatePending(true); Runnable uiUpdater = new Runnable() { public void run() { - updateAllUIs(); - setUpdatePending(false); + try { + updateAllUIs(); + } finally { + setUpdatePending(false); + } } }; SwingUtilities.invokeLater(uiUpdater); diff --git a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java index 95a9aed981a..0a0b25f9383 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,7 @@ import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicHTML; import javax.swing.text.View; +import com.sun.java.swing.SwingUtilities3; import sun.swing.MenuItemLayoutHelper; import sun.swing.SwingUtilities2; @@ -552,15 +553,6 @@ public class SynthGraphicsUtils { return result; } - static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) { - if (insets != null) { - rect.x += (leftToRight ? insets.left : insets.right); - rect.y += insets.top; - rect.width -= (leftToRight ? insets.right : insets.left) + rect.x; - rect.height -= (insets.bottom + rect.y); - } - } - static void paint(SynthContext context, SynthContext accContext, Graphics g, Icon checkIcon, Icon arrowIcon, String acceleratorDelimiter, int defaultTextIconGap, String propertyPrefix) { @@ -570,7 +562,7 @@ public class SynthGraphicsUtils { Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); boolean leftToRight = SynthLookAndFeel.isLeftToRight(mi); - applyInsets(viewRect, mi.getInsets(), leftToRight); + SwingUtilities3.applyInsets(viewRect, mi.getInsets(), leftToRight); SynthMenuItemLayoutHelper lh = new SynthMenuItemLayoutHelper( context, accContext, mi, checkIcon, arrowIcon, viewRect, diff --git a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java index 6cb4803c65f..700e3ed8a38 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -909,8 +909,11 @@ public class SynthLookAndFeel extends BasicLookAndFeel { Runnable uiUpdater = new Runnable() { @Override public void run() { - updateAllUIs(); - setUpdatePending(false); + try { + updateAllUIs(); + } finally { + setUpdatePending(false); + } } }; SwingUtilities.invokeLater(uiUpdater); diff --git a/src/java.desktop/share/classes/javax/swing/text/html/FormView.java b/src/java.desktop/share/classes/javax/swing/text/html/FormView.java index 47f10e1abb2..ad13f2b6657 100644 --- a/src/java.desktop/share/classes/javax/swing/text/html/FormView.java +++ b/src/java.desktop/share/classes/javax/swing/text/html/FormView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,7 @@ package javax.swing.text.html; import java.awt.Component; +import java.awt.MediaTracker; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -43,7 +44,6 @@ import javax.swing.Box; import javax.swing.ButtonModel; import javax.swing.ComboBoxModel; import javax.swing.DefaultButtonModel; -import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -273,15 +273,21 @@ public class FormView extends ComponentView implements ActionListener { maxIsPreferred = 3; } else if (type.equals("image")) { String srcAtt = (String) attr.getAttribute(HTML.Attribute.SRC); + String altAtt = (String) attr.getAttribute(HTML.Attribute.ALT); + if (altAtt == null) { + altAtt = srcAtt; + } JButton button; try { URL base = ((HTMLDocument)getElement().getDocument()).getBase(); @SuppressWarnings("deprecation") URL srcURL = new URL(base, srcAtt); - Icon icon = new ImageIcon(srcURL); - button = new JButton(icon); + ImageIcon icon = new ImageIcon(srcURL, altAtt); + button = icon.getImageLoadStatus() == MediaTracker.COMPLETE + ? new JButton(icon) + : new JButton(altAtt); } catch (MalformedURLException e) { - button = new JButton(srcAtt); + button = new JButton(altAtt); } if (model != null) { button.setModel((ButtonModel)model); diff --git a/src/java.desktop/share/classes/sun/font/TextLabelFactory.java b/src/java.desktop/share/classes/sun/font/TextLabelFactory.java index d245f33b8af..7a41bd2e7f5 100644 --- a/src/java.desktop/share/classes/sun/font/TextLabelFactory.java +++ b/src/java.desktop/share/classes/sun/font/TextLabelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -119,7 +119,7 @@ public final class TextLabelFactory { int start, int limit) { - if (start >= limit || start < lineStart || limit > lineLimit) { + if (start > limit || start < lineStart || limit > lineLimit) { throw new IllegalArgumentException("bad start: " + start + " or limit: " + limit); } @@ -145,7 +145,7 @@ public final class TextLabelFactory { int start, int limit) { - if (start >= limit || start < lineStart || limit > lineLimit) { + if (start > limit || start < lineStart || limit > lineLimit) { throw new IllegalArgumentException("bad start: " + start + " or limit: " + limit); } diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java b/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java index 2ce0cd4672c..9d2c8dc2a72 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -144,7 +144,9 @@ final class Curve { // finds points where the first and second derivative are // perpendicular. This happens when g(t) = f'(t)*f''(t) == 0 (where // * is a dot product). Unfortunately, we have to solve a cubic. - private int perpendiculardfddf(final double[] pts, final int off) { + private int perpendiculardfddf(final double[] pts, final int off, + final double A, final double B) + { assert pts.length >= off + 4; // these are the coefficients of some multiple of g(t) (not g(t), @@ -155,7 +157,7 @@ final class Curve { final double c = 2.0d * (dax * cx + day * cy) + dbx * dbx + dby * dby; final double d = dbx * cx + dby * cy; - return Helpers.cubicRootsInAB(a, b, c, d, pts, off, 0.0d, 1.0d); + return Helpers.cubicRootsInAB(a, b, c, d, pts, off, A, B); } // Tries to find the roots of the function ROC(t)-w in [0, 1). It uses @@ -171,35 +173,43 @@ final class Curve { // at most 4 sub-intervals of (0,1). ROC has asymptotes at inflection // points, so roc-w can have at least 6 roots. This shouldn't be a // problem for what we're trying to do (draw a nice looking curve). - int rootsOfROCMinusW(final double[] roots, final int off, final double w2, final double err) { + int rootsOfROCMinusW(final double[] roots, final int off, final double w2, + final double A, final double B) + { // no OOB exception, because by now off<=6, and roots.length >= 10 assert off <= 6 && roots.length >= 10; int ret = off; - final int end = off + perpendiculardfddf(roots, off); + final int end = off + perpendiculardfddf(roots, off, A, B); + Helpers.isort(roots, off, end); roots[end] = 1.0d; // always check interval end points - double t0 = 0.0d, ft0 = ROCsq(t0) - w2; + double t0 = 0.0d; + double ft0 = eliminateInf(ROCsq(t0) - w2); + double t1, ft1; for (int i = off; i <= end; i++) { - double t1 = roots[i], ft1 = ROCsq(t1) - w2; + t1 = roots[i]; + ft1 = eliminateInf(ROCsq(t1) - w2); if (ft0 == 0.0d) { roots[ret++] = t0; } else if (ft1 * ft0 < 0.0d) { // have opposite signs // (ROC(t)^2 == w^2) == (ROC(t) == w) is true because // ROC(t) >= 0 for all t. - roots[ret++] = falsePositionROCsqMinusX(t0, t1, w2, err); + roots[ret++] = falsePositionROCsqMinusX(t0, t1, ft0, ft1, w2, A); // A = err } t0 = t1; ft0 = ft1; } - return ret - off; } - private static double eliminateInf(final double x) { - return (x == Double.POSITIVE_INFINITY ? Double.MAX_VALUE : - (x == Double.NEGATIVE_INFINITY ? Double.MIN_VALUE : x)); + private final static double MAX_ROC_SQ = 1e20; + + private static double eliminateInf(final double x2) { + // limit the value of x to avoid numerical problems (smaller step): + // must handle NaN and +Infinity: + return (x2 <= MAX_ROC_SQ) ? x2 : MAX_ROC_SQ; } // A slight modification of the false position algorithm on wikipedia. @@ -210,17 +220,18 @@ final class Curve { // and turn out. Same goes for the newton's method // algorithm in Helpers.java private double falsePositionROCsqMinusX(final double t0, final double t1, + final double ft0, final double ft1, final double w2, final double err) { final int iterLimit = 100; int side = 0; - double t = t1, ft = eliminateInf(ROCsq(t) - w2); - double s = t0, fs = eliminateInf(ROCsq(s) - w2); + double s = t0, fs = eliminateInf(ft0); + double t = t1, ft = eliminateInf(ft1); double r = s, fr; - for (int i = 0; i < iterLimit && Math.abs(t - s) > err * Math.abs(t + s); i++) { + for (int i = 0; i < iterLimit && Math.abs(t - s) > err; i++) { r = (fs * t - ft * s) / (fs - ft); - fr = ROCsq(r) - w2; + fr = eliminateInf(ROCsq(r) - w2); if (sameSign(fr, ft)) { ft = fr; t = r; if (side < 0) { @@ -241,7 +252,7 @@ final class Curve { break; } } - return r; + return (Math.abs(ft) <= Math.abs(fs)) ? t : s; } private static boolean sameSign(final double x, final double y) { @@ -256,9 +267,9 @@ final class Curve { final double dy = t * (t * day + dby) + cy; final double ddx = 2.0d * dax * t + dbx; final double ddy = 2.0d * day * t + dby; - final double dx2dy2 = dx * dx + dy * dy; - final double ddx2ddy2 = ddx * ddx + ddy * ddy; - final double ddxdxddydy = ddx * dx + ddy * dy; - return dx2dy2 * ((dx2dy2 * dx2dy2) / (dx2dy2 * ddx2ddy2 - ddxdxddydy * ddxdxddydy)); + final double dx2dy2 = dx * dx + dy * dy; // positive + final double dxddyddxdy = dx * ddy - dy * ddx; + // may return +Infinity if dxddyddxdy = 0 or NaN if 0/0: + return (dx2dy2 * dx2dy2 * dx2dy2) / (dxddyddxdy * dxddyddxdy); // both positive } } diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java b/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java index 66eb9334e86..f829872a8a8 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java @@ -564,7 +564,7 @@ public final class DMarlinRenderingEngine extends RenderingEngine } private static boolean nearZero(final double num) { - return Math.abs(num) < 2.0d * Math.ulp(num); + return Math.abs(num) < 2.0d * Helpers.ulp(num); } abstract static class NormalizingPathIterator implements PathIterator { diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java index 0aed05ab506..926533cdb2b 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,12 +31,19 @@ import sun.java2d.marlin.stats.StatLong; final class Helpers implements MarlinConst { + private final static double T_ERR = 1e-4; + private final static double T_A = T_ERR; + private final static double T_B = 1.0 - T_ERR; + private static final double EPS = 1e-9d; private Helpers() { throw new Error("This is a non instantiable class"); } + /** use lower precision like former Pisces and Marlin (float-precision) */ + static double ulp(final double value) { return Math.ulp((float)value); } + static boolean within(final double x, final double y) { return within(x, y, EPS); } @@ -322,10 +329,10 @@ final class Helpers implements MarlinConst { // now we must subdivide at points where one of the offset curves will have // a cusp. This happens at ts where the radius of curvature is equal to w. - ret += c.rootsOfROCMinusW(ts, ret, w2, 0.0001d); + ret += c.rootsOfROCMinusW(ts, ret, w2, T_A, T_B); - ret = filterOutNotInAB(ts, 0, ret, 0.0001d, 0.9999d); - isort(ts, ret); + ret = filterOutNotInAB(ts, 0, ret, T_A, T_B); + isort(ts, 0, ret); return ret; } @@ -354,7 +361,7 @@ final class Helpers implements MarlinConst { if ((outCodeOR & OUTCODE_BOTTOM) != 0) { ret += curve.yPoints(ts, ret, clipRect[1]); } - isort(ts, ret); + isort(ts, 0, ret); return ret; } @@ -374,11 +381,11 @@ final class Helpers implements MarlinConst { } } - static void isort(final double[] a, final int len) { - for (int i = 1, j; i < len; i++) { + static void isort(final double[] a, final int off, final int len) { + for (int i = off + 1, j; i < len; i++) { final double ai = a[i]; j = i - 1; - for (; j >= 0 && a[j] > ai; j--) { + for (; j >= off && a[j] > ai; j--) { a[j + 1] = a[j]; } a[j + 1] = ai; diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java index 59f93ed7d6d..1c257bc13d9 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -886,8 +886,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { // if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4, // in which case ignore if p1 == p2 - final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Math.ulp(y2)); - final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Math.ulp(y4)); + final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Helpers.ulp(y2)); + final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Helpers.ulp(y4)); if (p1eqp2 && p3eqp4) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); @@ -905,7 +905,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { final double l1sq = dx1 * dx1 + dy1 * dy1; final double l4sq = dx4 * dx4 + dy4 * dy4; - if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Math.ulp(dotsq))) { + if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Helpers.ulp(dotsq))) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); } @@ -1078,8 +1078,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { // equal if they're very close to each other. // if p1 == p2 or p2 == p3: draw line from p1->p3 - final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Math.ulp(y2)); - final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Math.ulp(y3)); + final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Helpers.ulp(y2)); + final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Helpers.ulp(y3)); if (p1eqp2 || p2eqp3) { return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); @@ -1091,7 +1091,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { final double l1sq = dx12 * dx12 + dy12 * dy12; final double l3sq = dx23 * dx23 + dy23 * dy23; - if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Math.ulp(dotsq))) { + if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Helpers.ulp(dotsq))) { return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); } diff --git a/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java b/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java index b657e434d2f..949a914990b 100644 --- a/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java +++ b/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -73,8 +73,8 @@ public class OutlineTextRenderer implements TextPipe { public void drawString(SunGraphics2D g2d, String str, double x, double y) { - if ("".equals(str)) { - return; // TextLayout constructor throws IAE on "". + if (str.length() == 0) { + return; } TextLayout tl = new TextLayout(str, g2d.getFont(), g2d.getFontRenderContext()); diff --git a/src/java.desktop/share/classes/sun/print/PrintJob2D.java b/src/java.desktop/share/classes/sun/print/PrintJob2D.java index 8128dae1057..ee0caaa54fe 100644 --- a/src/java.desktop/share/classes/sun/print/PrintJob2D.java +++ b/src/java.desktop/share/classes/sun/print/PrintJob2D.java @@ -79,7 +79,12 @@ public class PrintJob2D extends PrintJob { * needs to implement PrintGraphics, so we wrap * the Graphics2D instance. */ - return new ProxyPrintGraphics(printJobDelegate.getGraphics(), this); + Graphics g = printJobDelegate.getGraphics(); + if (g == null) { // PrintJob.end() has been called. + return null; + } else { + return new ProxyPrintGraphics(g, this); + } } /** diff --git a/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java b/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java index 2a6d45e2ba8..754af87b94e 100644 --- a/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java +++ b/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java @@ -2470,13 +2470,16 @@ public abstract class RasterPrinterJob extends PrinterJob { g.setPaint(Color.black); } - /* On-screen drawString renders most control chars as the missing glyph - * and have the non-zero advance of that glyph. - * Exceptions are \t, \n and \r which are considered zero-width. - * This is a utility method used by subclasses to remove them so we - * don't have to worry about platform or font specific handling of them. + /** + * Removes ignorable whitespace from the specified text, so that there + * is no need for platform-specific or font-specific custom whitespace + * handling, and so that these characters are not treated like control + * characters which are printed as the missing glyph. + * + * @param s the text to process + * @return the input text, with ignorable whitespace (if any) removed */ - protected String removeControlChars(String s) { + public static String removeControlChars(String s) { char[] in_chars = s.toCharArray(); int len = in_chars.length; char[] out_chars = new char[len]; diff --git a/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java b/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java index 86d240a91fc..8af75f98b83 100644 --- a/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java +++ b/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -214,8 +214,11 @@ public class DesktopProperty implements UIDefaults.ActiveValue { setUpdatePending(true); Runnable uiUpdater = new Runnable() { public void run() { - updateAllUIs(); - setUpdatePending(false); + try { + updateAllUIs(); + } finally { + setUpdatePending(false); + } } }; SwingUtilities.invokeLater(uiUpdater); diff --git a/src/java.desktop/share/conf/psfontj2d.properties b/src/java.desktop/share/conf/psfontj2d.properties index 9efe8864428..8030a82bc4f 100644 --- a/src/java.desktop/share/conf/psfontj2d.properties +++ b/src/java.desktop/share/conf/psfontj2d.properties @@ -59,7 +59,6 @@ avantgarde_book_oblique=avantgarde_book_oblique avantgarde_demi_oblique=avantgarde_demi_oblique # itcavantgarde=avantgarde_book -itcavantgarde=avantgarde_book itcavantgarde_demi=avantgarde_demi itcavantgarde_oblique=avantgarde_book_oblique itcavantgarde_demi_oblique=avantgarde_demi_oblique diff --git a/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c b/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c index cbdad61f78e..4f2cfca8dd0 100644 --- a/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c +++ b/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c @@ -279,7 +279,9 @@ SplashDecodeGif(Splash * splash, GifFileType * gif) ImageRect dstRect; rgbquad_t fillColor = 0; // 0 is transparent - if (transparentColor < 0) { + if (colorMap && + colorMap->Colors && + transparentColor < 0) { fillColor= MAKE_QUAD_GIF( colorMap->Colors[gif->SBackGroundColor], 0xff); } diff --git a/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java b/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java index d96894660b9..f9445a78ede 100644 --- a/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java +++ b/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java @@ -876,12 +876,16 @@ public final class PrintServiceLookupProvider extends PrintServiceLookup FileReader reader = new FileReader(f); bufferedReader = new BufferedReader(reader); String line; + results = new ArrayList<>(); while ((line = bufferedReader.readLine()) != null) { results.add(line); } } - } finally { + } catch (Exception e) { + // Print exception for tracking printer command errors + IPPPrintService.debug_println("Printer command error: " + e); + } finally { f.delete(); // promptly close all streams. if (bufferedReader != null) { diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c b/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c index 1f73a3256b0..5985dd93226 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c @@ -1268,11 +1268,15 @@ Java_sun_awt_X11GraphicsDevice_pGetBounds(JNIEnv *env, jobject this, jint screen xinInfo[screen].width, xinInfo[screen].height); XFree(xinInfo); + if (!bounds) { + return NULL; + } } } else { jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); if (exceptionClass != NULL) { (*env)->ThrowNew(env, exceptionClass, "Illegal screen index"); + return NULL; } } } diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c b/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c index 916880873c6..e5b2dfa6db9 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c @@ -276,10 +276,7 @@ GtkApi* gtk3_load(JNIEnv *env, const char* lib_name) fp_gtk_check_version = dl_symbol("gtk_check_version"); /* GLib */ - fp_glib_check_version = dlsym(gtk3_libhandle, "glib_check_version"); - if (!fp_glib_check_version) { - dlerror(); - } + fp_glib_check_version = dl_symbol("glib_check_version"); fp_g_free = dl_symbol("g_free"); fp_g_object_unref = dl_symbol("g_object_unref"); diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java b/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java index 75e8fa42da0..87b1591c0eb 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -73,6 +73,7 @@ import sun.font.TrueTypeFont; import sun.print.PathGraphics; import sun.print.ProxyGraphics2D; +import sun.print.RasterPrinterJob; final class WPathGraphics extends PathGraphics { @@ -847,7 +848,7 @@ final class WPathGraphics extends PathGraphics { * removed now so the string and positions are the same length. * For other cases we need to pass glyph codes to GDI. */ - str = wPrinterJob.removeControlChars(str); + str = RasterPrinterJob.removeControlChars(str); char[] chars = str.toCharArray(); int len = chars.length; GlyphVector gv = null; diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java b/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java index e50cfcff33b..17d41036bcc 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1202,14 +1202,6 @@ public final class WPrinterJob extends RasterPrinterJob } } - /** - * Remove control characters. - */ - @Override - protected String removeControlChars(String str) { - return super.removeControlChars(str); - } - /** * Draw the string {@code text} to the printer's * device context at the specified position. diff --git a/src/java.naming/share/classes/com/sun/jndi/ldap/Connection.java b/src/java.naming/share/classes/com/sun/jndi/ldap/Connection.java index 8166fe97a4a..6eebaf4d6eb 100644 --- a/src/java.naming/share/classes/com/sun/jndi/ldap/Connection.java +++ b/src/java.naming/share/classes/com/sun/jndi/ldap/Connection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,6 @@ import java.net.Socket; import javax.net.ssl.SSLSocket; import javax.naming.CommunicationException; -import javax.naming.ServiceUnavailableException; import javax.naming.NamingException; import javax.naming.InterruptedNamingException; @@ -439,53 +438,22 @@ public final class Connection implements Runnable { * Reads a reply; waits until one is ready. */ BerDecoder readReply(LdapRequest ldr) throws NamingException { - BerDecoder rber; - - // If socket closed, don't even try - lock.lock(); - try { - if (sock == null) { - throw new ServiceUnavailableException(host + ":" + port + - "; socket closed"); - } - } finally { - lock.unlock(); - } - - IOException ioException = null; try { // if no timeout is set so we wait infinitely until // a response is received OR until the connection is closed or cancelled // http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-ldap.html#PROP - rber = ldr.getReplyBer(readTimeout); + return ldr.getReplyBer(readTimeout); } catch (InterruptedException ex) { throw new InterruptedNamingException( "Interrupted during LDAP operation"); } catch (IOException ioe) { - // Connection is timed out OR closed/cancelled - // getReplyBer throws IOException when the requests needs to be abandoned - ioException = ioe; - rber = null; - } - - if (rber == null) { + // getReplyBer() throws IOException when request needs to be abandoned abandonRequest(ldr, null); - } - // ioException can be not null in the following cases: - // a) The response is timed-out - // b) LDAP request connection has been closed - // If the request has been cancelled - CommunicationException is - // thrown directly from LdapRequest.getReplyBer, since there is no - // need to abandon request. - // The exception message is initialized in LdapRequest::getReplyBer - if (ioException != null) { - // Throw CommunicationException after all cleanups are done - String message = ioException.getMessage(); - var ce = new CommunicationException(message); - ce.initCause(ioException); + // rethrow as CommunicationException (which is a NamingException) + var ce = new CommunicationException(ioe.getMessage()); + ce.initCause(ioe); throw ce; } - return rber; } //////////////////////////////////////////////////////////////////////////// diff --git a/src/java.naming/share/classes/com/sun/jndi/ldap/LdapRequest.java b/src/java.naming/share/classes/com/sun/jndi/ldap/LdapRequest.java index 2242d17ebf7..2fdd685376c 100644 --- a/src/java.naming/share/classes/com/sun/jndi/ldap/LdapRequest.java +++ b/src/java.naming/share/classes/com/sun/jndi/ldap/LdapRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,24 +30,22 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import javax.naming.CommunicationException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; final class LdapRequest { - private static final BerDecoder EOF = new BerDecoder(new byte[]{}, -1, 0); + private static final BerDecoder CLOSED_MARKER = new BerDecoder(new byte[]{}, -1, 0); + private static final BerDecoder CANCELLED_MARKER = new BerDecoder(new byte[]{}, -1, 0); private static final String CLOSE_MSG = "LDAP connection has been closed"; - private static final String TIMEOUT_MSG_FMT = "LDAP response read timed out, timeout used: %d ms."; LdapRequest next; // Set/read in synchronized Connection methods final int msgId; // read-only private final BlockingQueue replies; + private final boolean pauseAfterReceipt; + private volatile boolean cancelled; private volatile boolean closed; private volatile boolean completed; - private final boolean pauseAfterReceipt; - // LdapRequest instance lock - private final ReentrantLock lock = new ReentrantLock(); LdapRequest(int msgId, boolean pause, int replyQueueCapacity) { this.msgId = msgId; @@ -61,87 +59,94 @@ final class LdapRequest { void cancel() { cancelled = true; - replies.offer(EOF); + replies.offer(CANCELLED_MARKER); } void close() { - lock.lock(); - try { - closed = true; - replies.offer(EOF); - } finally { - lock.unlock(); - } - } - - private boolean isClosed() { - return closed && (replies.size() == 0 || replies.peek() == EOF); + closed = true; + replies.offer(CLOSED_MARKER); } boolean addReplyBer(BerDecoder ber) { - lock.lock(); - try { - // check the closed boolean value here as we don't want anything - // to be added to the queue after close() has been called. - if (cancelled || closed) { - return false; - } - - // peek at the BER buffer to check if it is a SearchResultDone PDU + // check if the request is closed or cancelled, if yes then don't + // add the reply since it won't be returned back later through getReplyBer(). + // this is merely a best effort basis check and if we do add the reply + // due to a race, that's OK since the replies queue would have necessary + // markers for cancelled/closed state and those will be detected by getReplyBer(). + if (cancelled || closed) { + return false; + } + // if the request is not already completed, check if the reply being added + // is a LDAP_REP_RESULT, representing a SearchResultDone PDU + if (!completed) { + boolean isLdapResResult = false; try { ber.parseSeq(null); ber.parseInt(); - completed = (ber.peekByte() == LdapClient.LDAP_REP_RESULT); + isLdapResResult = (ber.peekByte() == LdapClient.LDAP_REP_RESULT); } catch (IOException e) { // ignore } ber.reset(); - // Add a new reply to the queue of unprocessed replies. - try { - replies.put(ber); - } catch (InterruptedException e) { - // ignore + if (isLdapResResult) { + completed = true; } - - return pauseAfterReceipt; - } finally { - lock.unlock(); } + + // Add a new reply to the queue of unprocessed replies. + try { + replies.put(ber); + } catch (InterruptedException e) { + // ignore + } + return pauseAfterReceipt; } /** * Read reply BER * @param millis timeout, infinite if the value is negative * @return BerDecoder if reply was read successfully - * @throws CommunicationException request has been canceled and request does not need to be abandoned - * @throws IOException request has been closed or timed out. Request does need to be abandoned - * @throws InterruptedException LDAP operation has been interrupted + * @throws CommunicationException request has been canceled and request + * does not need to be abandoned (i.e. a LDAP_REQ_ABANDON + * message need not be sent across) + * @throws IOException request has been closed or timed out. + * Request needs to be abandoned (i.e. a LDAP_REQ_ABANDON + * message needs to be sent across) + * @throws InterruptedException the wait to read a reply has been interrupted */ + // more than one thread invoking this method concurrently isn't expected BerDecoder getReplyBer(long millis) throws IOException, CommunicationException, InterruptedException { - if (cancelled) { - throw new CommunicationException("Request: " + msgId + - " cancelled"); - } - if (isClosed()) { - throw new IOException(CLOSE_MSG); - } - BerDecoder result = millis > 0 ? - replies.poll(millis, TimeUnit.MILLISECONDS) : replies.take(); - - if (cancelled) { - throw new CommunicationException("Request: " + msgId + - " cancelled"); + final boolean hasReplies = replies.peek() != null; + if (!hasReplies) { + // no replies have been queued, so if the request has + // been cancelled or closed, then raise an exception + if (cancelled) { + throw new CommunicationException("Request: " + msgId + + " cancelled"); + } + if (closed) { + throw new IOException(CLOSE_MSG); + } } - + // either there already are queued replies or the request is still + // alive (i.e. not cancelled or closed). we wait for a reply to arrive + // or the request to be cancelled/closed, in which case the replies + // queue will contain the relevant marker. + final BerDecoder result = millis > 0 + ? replies.poll(millis, TimeUnit.MILLISECONDS) + : replies.take(); // poll from 'replies' blocking queue ended-up with timeout if (result == null) { - throw new IOException(String.format(TIMEOUT_MSG_FMT, millis)); + throw new IOException("LDAP response read timed out, timeout used: " + millis + " ms."); } - // Unexpected EOF can be caused by connection closure or cancellation - if (result == EOF) { + if (result == CANCELLED_MARKER) { + throw new CommunicationException("Request: " + msgId + + " cancelled"); + } + if (result == CLOSED_MARKER) { throw new IOException(CLOSE_MSG); } return result; diff --git a/src/java.net.http/share/classes/java/net/http/HttpClient.java b/src/java.net.http/share/classes/java/net/http/HttpClient.java index 59afff013c7..4ce77486e70 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpClient.java +++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java @@ -28,9 +28,11 @@ package java.net.http; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetAddress; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; +import java.net.URI; import java.nio.channels.Selector; import java.net.Authenticator; import java.net.CookieHandler; @@ -59,7 +61,7 @@ import jdk.internal.net.http.HttpClientBuilderImpl; * The {@link #newBuilder() newBuilder} method returns a builder that creates * instances of the default {@code HttpClient} implementation. * The builder can be used to configure per-client state, like: the preferred - * protocol version ( HTTP/1.1 or HTTP/2 ), whether to follow redirects, a + * protocol version ( HTTP/1.1, HTTP/2 or HTTP/3 ), whether to follow redirects, a * proxy, an authenticator, etc. Once built, an {@code HttpClient} is immutable, * and can be used to send multiple requests. * @@ -162,6 +164,59 @@ import jdk.internal.net.http.HttpClientBuilderImpl; * prevent the resources allocated by the associated client from * being reclaimed by the garbage collector. * + *

    + * The default implementation of the {@code HttpClient} supports HTTP/1.1, + * HTTP/2, and HTTP/3. Which version of the protocol is actually used when sending + * a request can depend on multiple factors. In the case of HTTP/2, it may depend + * on an initial upgrade to succeed (when using a plain connection), or on HTTP/2 + * being successfully negotiated during the Transport Layer Security (TLS) handshake. + * + *

    If {@linkplain Version#HTTP_2 HTTP/2} is selected over a clear + * connection, and no HTTP/2 connection to the + * origin server + * already exists, the client will create a new connection and attempt an upgrade + * from HTTP/1.1 to HTTP/2. + * If the upgrade succeeds, then the response to this request will use HTTP/2. + * If the upgrade fails, then the response will be handled using HTTP/1.1. + * + *

    Other constraints may also affect the selection of protocol version. + * For example, if HTTP/2 is requested through a proxy, and if the implementation + * does not support this mode, then HTTP/1.1 may be used. + *

    + * The HTTP/3 protocol is not selected by default, but can be enabled by setting + * the {@linkplain Builder#version(Version) HttpClient preferred version} or the + * {@linkplain HttpRequest.Builder#version(Version) HttpRequest preferred version} to + * {@linkplain Version#HTTP_3 HTTP/3}. Like for HTTP/2, which protocol version is + * actually used when HTTP/3 is enabled may depend on several factors. + * {@linkplain HttpOption#H3_DISCOVERY Configuration hints} can + * be {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) provided} + * to help the {@code HttpClient} implementation decide how to establish + * and carry out the HTTP exchange when the HTTP/3 protocol is enabled. + * If no configuration hints are provided, the {@code HttpClient} will select + * one as explained in the {@link HttpOption#H3_DISCOVERY H3_DISCOVERY} + * option API documentation. + *
    Note that a request whose {@linkplain URI#getScheme() URI scheme} is not + * {@code "https"} will never be sent over HTTP/3. In this implementation, + * HTTP/3 is not used if a proxy is selected. + * + *

    + * If a concrete instance of {@link HttpClient} doesn't support sending a + * request through HTTP/3, an {@link UnsupportedProtocolVersionException} may be + * thrown, either when {@linkplain Builder#build() building} the client with + * a {@linkplain Builder#version(Version) preferred version} set to HTTP/3, + * or when attempting to send a request with {@linkplain HttpRequest.Builder#version(Version) + * HTTP/3 enabled} when {@link Http3DiscoveryMode#HTTP_3_URI_ONLY HTTP_3_URI_ONLY} + * was {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) specified}. + * This may typically happen if the {@link #sslContext() SSLContext} + * or {@link #sslParameters() SSLParameters} configured on the client instance cannot + * be used with HTTP/3. + * + * @see UnsupportedProtocolVersionException + * @see Builder#version(Version) + * @see HttpRequest.Builder#version(Version) + * @see HttpRequest.Builder#setOption(HttpOption, Object) + * @see HttpOption#H3_DISCOVERY + * * @since 11 */ public abstract class HttpClient implements AutoCloseable { @@ -320,23 +375,19 @@ public abstract class HttpClient implements AutoCloseable { public Builder followRedirects(Redirect policy); /** - * Requests a specific HTTP protocol version where possible. + * Sets the default preferred HTTP protocol version for requests + * issued by this client. * *

    If this method is not invoked prior to {@linkplain #build() * building}, then newly built clients will prefer {@linkplain * Version#HTTP_2 HTTP/2}. * - *

    If set to {@linkplain Version#HTTP_2 HTTP/2}, then each request - * will attempt to upgrade to HTTP/2. If the upgrade succeeds, then the - * response to this request will use HTTP/2 and all subsequent requests - * and responses to the same - * origin server - * will use HTTP/2. If the upgrade fails, then the response will be - * handled using HTTP/1.1 + *

    If a request doesn't have a preferred version, then + * the effective preferred version for the request is the + * client's preferred version.

    * - * @implNote Constraints may also affect the selection of protocol version. - * For example, if HTTP/2 is requested through a proxy, and if the implementation - * does not support this mode, then HTTP/1.1 may be used + * @implNote Some constraints may also affect the {@linkplain + * HttpClient##ProtocolVersionSelection selection of the actual protocol version}. * * @param version the requested HTTP protocol version * @return this builder @@ -439,9 +490,14 @@ public abstract class HttpClient implements AutoCloseable { * @return a new {@code HttpClient} * * @throws UncheckedIOException may be thrown if underlying IO resources required - * by the implementation cannot be allocated. For instance, + * by the implementation cannot be allocated, or if the resulting configuration + * does not satisfy the implementation requirements. For instance, * if the implementation requires a {@link Selector}, and opening - * one fails due to {@linkplain Selector#open() lack of necessary resources}. + * one fails due to {@linkplain Selector#open() lack of necessary resources}, + * or if the {@linkplain #version(Version) preferred protocol version} is not + * {@linkplain HttpClient##UnsupportedProtocolVersion supported by + * the implementation or cannot be used in this configuration}. + * */ public HttpClient build(); } @@ -525,9 +581,11 @@ public abstract class HttpClient implements AutoCloseable { * Returns the preferred HTTP protocol version for this client. The default * value is {@link HttpClient.Version#HTTP_2} * - * @implNote Constraints may also affect the selection of protocol version. - * For example, if HTTP/2 is requested through a proxy, and if the - * implementation does not support this mode, then HTTP/1.1 may be used + * @implNote + * The protocol version that the {@code HttpClient} eventually + * decides to use for a request is affected by various factors noted + * in {@linkplain ##ProtocolVersionSelection protocol version selection} + * section. * * @return the HTTP protocol version requested */ @@ -562,7 +620,13 @@ public abstract class HttpClient implements AutoCloseable { /** * HTTP version 2 */ - HTTP_2 + HTTP_2, + + /** + * HTTP version 3 + * @since 26 + */ + HTTP_3 } /** diff --git a/src/java.net.http/share/classes/java/net/http/HttpOption.java b/src/java.net.http/share/classes/java/net/http/HttpOption.java new file mode 100644 index 00000000000..cbff11f71ee --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/HttpOption.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest.Builder; + +/** + * This interface is used to provide additional request configuration + * option hints on how an HTTP request/response exchange should + * be carried out by the {@link HttpClient} implementation. + * Request configuration option hints can be provided to an + * {@link HttpRequest} with the {@link + * Builder#setOption(HttpOption, Object) HttpRequest.Builder + * setOption} method. + * + *

    Concrete instances of this class and its subclasses are immutable. + * + * @apiNote + * In this version, the {@code HttpOption} interface is sealed and + * only allows the {@link #H3_DISCOVERY} option. However, it could be + * extended in the future to support additional options. + *

    + * The {@link #H3_DISCOVERY} option can be used to help the + * {@link HttpClient} decide how to select or establish an + * HTTP/3 connection through which to carry out an HTTP/3 + * request/response exchange. + * + * @param The {@linkplain #type() type of the option value} + * + * @since 26 + */ +public sealed interface HttpOption permits HttpRequestOptionImpl { + /** + * {@return the option name} + * + * @implSpec Different options must have different names. + */ + String name(); + + /** + * {@return the type of the value associated with the option} + * + * @apiNote Different options may have the same type. + */ + Class type(); + + /** + * An option that can be used to configure how the {@link HttpClient} will + * select or establish an HTTP/3 connection through which to carry out + * the request. If {@link Version#HTTP_3} is not selected either as + * the {@linkplain Builder#version(Version) request preferred version} + * or the {@linkplain HttpClient.Builder#version(Version) HttpClient + * preferred version} setting this option on the request has no effect. + *

    + * The {@linkplain #name() name of this option} is {@code "H3_DISCOVERY"}. + * + * @implNote + * The JDK built-in implementation of the {@link HttpClient} understands the + * request option {@link #H3_DISCOVERY} hint. + *
    + * If no {@code H3_DISCOVERY} hint is provided, and the {@linkplain Version#HTTP_3 + * HTTP/3 version} is selected, either as {@linkplain Builder#version(Version) + * request preferred version} or {@linkplain HttpClient.Builder#version(Version) + * client preferred version}, the JDK built-in implementation will establish + * the exchange as per {@link Http3DiscoveryMode#ANY}. + *

    + * In case of {@linkplain HttpClient.Redirect redirect}, the + * {@link #H3_DISCOVERY} option, if present, is always transferred to + * the new request. + *

    + * In this implementation, HTTP/3 through proxies is not supported. + * Unless {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, if + * a {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy} is {@linkplain + * ProxySelector#select(URI) selected} for the {@linkplain HttpRequest#uri() + * request URI}, the protocol version is downgraded to HTTP/2 or + * HTTP/1.1 and the {@link #H3_DISCOVERY} option is ignored. If, on the + * other hand, {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, + * the request will fail. + * + * @see Http3DiscoveryMode + * @see Builder#setOption(HttpOption, Object) + */ + HttpOption H3_DISCOVERY = + new HttpRequestOptionImpl<>(Http3DiscoveryMode.class, "H3_DISCOVERY"); + + /** + * This enumeration can be used to help the {@link HttpClient} decide + * how an HTTP/3 exchange should be established, and can be provided + * as the value of the {@link HttpOption#H3_DISCOVERY} option + * to {@link Builder#setOption(HttpOption, Object) Builder.setOption}. + *

    + * Note that if neither the {@linkplain Builder#version(Version) request preferred + * version} nor the {@linkplain HttpClient.Builder#version(Version) client preferred + * version} is {@linkplain Version#HTTP_3 HTTP/3}, no HTTP/3 exchange will + * be established and the {@code Http3DiscoveryMode} is ignored. + * + * @since 26 + */ + enum Http3DiscoveryMode { + /** + * This instructs the {@link HttpClient} to use its own implementation + * specific algorithm to find or establish a connection for the exchange. + * Typically, if no connection was previously established with the origin + * server defined by the request URI, the {@link HttpClient} implementation + * may attempt to establish both an HTTP/3 connection over QUIC and an HTTP + * connection over TLS/TCP at the authority present in the request URI, + * and use the first that succeeds. The exchange may then be carried out with + * any of the {@linkplain Version + * three HTTP protocol versions}, depending on which method succeeded first. + * + * @implNote + * If the {@linkplain Builder#version(Version) request preferred version} is {@linkplain + * Version#HTTP_3 HTTP/3}, the {@code HttpClient} may give priority to HTTP/3 by + * attempting to establish an HTTP/3 connection, before attempting a TLS + * connection over TCP. + *

    + * When attempting an HTTP/3 connection in this mode, the {@code HttpClient} may + * use any HTTP Alternative Services + * information it may have previously obtained from the origin server. If no + * such information is available, a direct HTTP/3 connection at the authority (host, port) + * present in the {@linkplain HttpRequest#uri() request URI} will be attempted. + */ + ANY, + /** + * This instructs the {@link HttpClient} to only use the + * HTTP Alternative Services + * to find or establish an HTTP/3 connection with the origin server. + * The exchange may then be carried out with any of the {@linkplain + * Version three HTTP protocol versions}, depending on + * whether an Alternate Service record for HTTP/3 could be found, and which HTTP version + * was negotiated with the origin server, if no such record could be found. + *

    + * In this mode, requests sent to the origin server will be sent through HTTP/1.1 or HTTP/2 + * until a {@code h3} HTTP Alternative Services + * endpoint for that server is advertised to the client. Usually, an alternate service is + * advertised by a server when responding to a request, so that subsequent requests can make + * use of that alternative service. + */ + ALT_SVC, + /** + * This instructs the {@link HttpClient} to only attempt an HTTP/3 connection + * with the origin server. The connection will only succeed if the origin server + * is listening for incoming HTTP/3 connections over QUIC at the same authority (host, port) + * as defined in the {@linkplain HttpRequest#uri() request URI}. In this mode, + * HTTP Alternative Services + * are not used. + */ + HTTP_3_URI_ONLY + } + +} diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequest.java b/src/java.net.http/share/classes/java/net/http/HttpRequest.java index 84a521336b6..c56328ba4b4 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java +++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java @@ -29,6 +29,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.http.HttpClient.Version; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; @@ -91,6 +92,24 @@ public abstract class HttpRequest { */ protected HttpRequest() {} + /** + * {@return the value configured on this request for the given option, if any} + * @param option a request configuration option + * @param the type of the option + * + * @see Builder#setOption(HttpOption, Object) + * + * @implSpec + * The default implementation of this method returns {@link Optional#empty()} + * if {@code option} is non-null, otherwise throws {@link NullPointerException}. + * + * @since 26 + */ + public Optional getOption(HttpOption option) { + Objects.requireNonNull(option); + return Optional.empty(); + } + /** * A builder of {@linkplain HttpRequest HTTP requests}. * @@ -144,14 +163,53 @@ public abstract class HttpRequest { * *

    The corresponding {@link HttpResponse} should be checked for the * version that was actually used. If the version is not set in a - * request, then the version requested will be that of the sending - * {@link HttpClient}. + * request, then the version requested will be {@linkplain + * HttpClient.Builder#version(Version) that of the sending + * {@code HttpClient}}. + * + * @implNote + * Constraints may also affect the {@linkplain HttpClient##ProtocolVersionSelection + * selection of the actual protocol version}. * * @param version the HTTP protocol version requested * @return this builder */ public Builder version(HttpClient.Version version); + /** + * Provides request configuration option hints modeled as key value pairs + * to help an {@link HttpClient} implementation decide how the + * request/response exchange should be established or carried out. + * + *

    An {@link HttpClient} implementation may decide to ignore request + * configuration option hints, or fail the request, if provided with any + * option hints that it does not understand. + *

    + * If this method is invoked twice for the same {@linkplain HttpOption + * request option}, any value previously provided to this builder for the + * corresponding option is replaced by the new value. + * If {@code null} is supplied as a value, any value previously + * provided is discarded. + * + * @implSpec + * The default implementation of this method discards the provided option + * hint and does nothing. + * + * @implNote + * The JDK built-in implementation of the {@link HttpClient} understands the + * request option {@link HttpOption#H3_DISCOVERY} hint. + * + * @param option the request configuration option + * @param value the request configuration option value (can be null) + * + * @return this builder + * + * @see HttpRequest#getOption(HttpOption) + * + * @since 26 + */ + public default Builder setOption(HttpOption option, T value) { return this; } + /** * Adds the given name value pair to the set of headers for this request. * The given value is added to the list of values for that name. @@ -394,6 +452,8 @@ public abstract class HttpRequest { } } ); + request.getOption(HttpOption.H3_DISCOVERY) + .ifPresent(opt -> builder.setOption(HttpOption.H3_DISCOVERY, opt)); return builder; } diff --git a/src/jdk.hotspot.agent/test/libproc/LibprocTest.java b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java similarity index 64% rename from src/jdk.hotspot.agent/test/libproc/LibprocTest.java rename to src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java index a3e27a94ec0..f5562c7068b 100644 --- a/src/jdk.hotspot.agent/test/libproc/LibprocTest.java +++ b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,23 +21,14 @@ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. - * */ +package java.net.http; -/** - This is test case run by debuggee for running LibprocClient.java. -*/ - -public class LibprocTest { - public static void main(String[] args) throws Exception { - String myStr = ""; - System.out.println("main start"); - synchronized(myStr) { - try { - myStr.wait(); - } catch (InterruptedException ee) { - } - } - System.out.println("main end"); - } +// Package private implementation of HttpRequest options +record HttpRequestOptionImpl(Class type, String name) + implements HttpOption { + @Override + public String toString() { + return name(); + } } diff --git a/src/java.net.http/share/classes/java/net/http/HttpResponse.java b/src/java.net.http/share/classes/java/net/http/HttpResponse.java index 52f5298452a..9843e4c7c5b 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpResponse.java +++ b/src/java.net.http/share/classes/java/net/http/HttpResponse.java @@ -803,24 +803,66 @@ public interface HttpResponse { /** * A handler for push promises. * - *

    A push promise is a synthetic request sent by an HTTP/2 server + *

    A push promise is a synthetic request sent by an HTTP/2 or HTTP/3 server * when retrieving an initiating client-sent request. The server has * determined, possibly through inspection of the initiating request, that * the client will likely need the promised resource, and hence pushes a * synthetic push request, in the form of a push promise, to the client. The * client can choose to accept or reject the push promise request. * - *

    A push promise request may be received up to the point where the + *

    For HTTP/2, a push promise request may be received up to the point where the * response body of the initiating client-sent request has been fully * received. The delivery of a push promise response, however, is not * coordinated with the delivery of the response to the initiating - * client-sent request. + * client-sent request. These are delivered with the + * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)} method. + *

    + * For HTTP/3, push promises are handled in a similar way, except that promises + * of the same resource (request URI, request headers and response body) can be + * promised multiple times, but are only delivered by the server (and this API) + * once though the method {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)}. + * Subsequent promises of the same resource, receive a notification only + * of the promise by the method {@link #notifyAdditionalPromise(HttpRequest, PushId)}. + * The same {@link PushPromiseHandler.PushId} is supplied for each of these + * notifications. Additionally, HTTP/3 push promises are not restricted to a context + * of a single initiating request. The same push promise can be delivered and then notified + * across multiple client initiated requests within the same HTTP/3 (QUIC) connection. * * @param the push promise response body type * @since 11 */ public interface PushPromiseHandler { + /** + * Represents a HTTP/3 PushID. PushIds can be shared across + * multiple client initiated requests on the same HTTP/3 connection. + * @since 26 + */ + public sealed interface PushId { + + /** + * Represents an HTTP/3 PushId. + * + * @param pushId the pushId as a long + * @param connectionLabel the {@link HttpResponse#connectionLabel()} + * of the HTTP/3 connection + * @apiNote + * The {@code connectionLabel} should be considered opaque, and ensures that + * two long pushId emitted by different connections correspond to distinct + * instances of {@code PushId}. The {@code pushId} corresponds to the + * unique push ID assigned by the server that identifies a given server + * push on that connection, as defined by + * RFC 9114, + * section 4.6 + * + * @spec https://www.rfc-editor.org/info/rfc9114 + * RFC 9114: HTTP/3 + * + * @since 26 + */ + record Http3PushId(long pushId, String connectionLabel) implements PushId { } + } + /** * Notification of an incoming push promise. * @@ -838,6 +880,12 @@ public interface HttpResponse { * then the push promise is rejected. The {@code acceptor} function will * throw an {@code IllegalStateException} if invoked more than once. * + *

    This method is invoked for all HTTP/2 push promises and also + * by default for the first promise of all HTTP/3 push promises. + * If {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)} + * is overridden, then this method is not directly invoked for HTTP/3 + * push promises. + * * @param initiatingRequest the initiating client-send request * @param pushPromiseRequest the synthetic push request * @param acceptor the acceptor function that must be successfully @@ -849,6 +897,67 @@ public interface HttpResponse { Function,CompletableFuture>> acceptor ); + /** + * Notification of the first occurrence of an HTTP/3 incoming push promise. + * + * Subsequent promises of the same resource (with the same PushId) are notified + * using {@link #notifyAdditionalPromise(HttpRequest, PushId) + * notifyAdditionalPromise(HttpRequest, PushId)}. + * + *

    This method is invoked once for each push promise received, up + * to the point where the response body of the initiating client-sent + * request has been fully received. + * + *

    A push promise is accepted by invoking the given {@code acceptor} + * function. The {@code acceptor} function must be passed a non-null + * {@code BodyHandler}, that is to be used to handle the promise's + * response body. The acceptor function will return a {@code + * CompletableFuture} that completes with the promise's response. + * + *

    If the {@code acceptor} function is not successfully invoked, + * then the push promise is rejected. The {@code acceptor} function will + * throw an {@code IllegalStateException} if invoked more than once. + * + * @implSpec the default implementation invokes + * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)}. This allows + * {@code PushPromiseHandlers} from previous releases to handle HTTP/3 push + * promise in a reasonable way. + * + * @param initiatingRequest the client request that resulted in the promise + * @param pushPromiseRequest the promised HttpRequest from the server + * @param pushid the PushId which can be linked to subsequent notifications + * @param acceptor the acceptor function that must be successfully + * invoked to accept the push promise + * + * @since 26 + */ + public default void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function,CompletableFuture>> acceptor + ) { + applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor); + } + + /** + * Invoked for each additional HTTP/3 Push Promise. The {@code pushid} links the promise to the + * original promised {@link HttpRequest} and {@link HttpResponse}. Additional promises + * generally result from different client initiated requests. + * + * @implSpec + * The default implementation of this method does nothing. + * + * @param initiatingRequest the client initiated request which resulted in the push + * @param pushid the pushid which may have been notified previously + * + * @since 26 + */ + public default void notifyAdditionalPromise( + HttpRequest initiatingRequest, + PushId pushid + ) { + } /** * Returns a push promise handler that accumulates push promises, and @@ -915,7 +1024,7 @@ public interface HttpResponse { * * @apiNote To ensure that all resources associated with the corresponding * HTTP exchange are properly released, an implementation of {@code - * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request + * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request(long) * request} more data until one of {@link #onComplete() onComplete} or * {@link #onError(Throwable) onError} are signalled, or {@link * Flow.Subscription#cancel cancel} its {@linkplain @@ -957,7 +1066,7 @@ public interface HttpResponse { * {@snippet : * // Streams the response body to a File * HttpResponse response = client - * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html")); } + * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html"))); } * * {@snippet : * // Accumulates the response body and returns it as a byte[] diff --git a/src/java.net.http/share/classes/java/net/http/StreamLimitException.java b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java new file mode 100644 index 00000000000..583b515b01b --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.net.http.HttpClient.Version; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.util.Objects; + +/** + * An exception raised when the limit imposed for stream creation on an + * HTTP connection is reached, and the client is unable to create a new + * stream. + *

    + * A {@code StreamLimitException} may be raised when attempting to send + * a new request on any {@linkplain #version() + * protocol version} that supports multiplexing on a single connection. Both + * {@linkplain HttpClient.Version#HTTP_2 HTTP/2} and {@linkplain + * HttpClient.Version#HTTP_3 HTTP/3} allow multiplexing concurrent requests + * to the same server on a single connection. Each request/response exchange + * is carried over a single stream, as defined by the corresponding + * protocol. + *

    + * Whether and when a {@code StreamLimitException} may be + * relayed to the code initiating a request/response exchange is + * implementation and protocol version dependent. + * + * @see HttpClient#send(HttpRequest, BodyHandler) + * @see HttpClient#sendAsync(HttpRequest, BodyHandler) + * @see HttpClient#sendAsync(HttpRequest, BodyHandler, PushPromiseHandler) + * + * @since 26 + */ +public final class StreamLimitException extends IOException { + + @java.io.Serial + private static final long serialVersionUID = 2614981180406031159L; + + /** + * The version of the HTTP protocol on which the stream limit exception occurred. + * Must not be null. + * @serial + */ + private final Version version; + + /** + * Creates a new {@code StreamLimitException} + * @param version the version of the protocol on which the stream limit exception + * occurred. Must not be null. + * @param message the detailed exception message, which can be null. + */ + public StreamLimitException(final Version version, final String message) { + super(message); + this.version = Objects.requireNonNull(version); + } + + /** + * {@return the protocol version for which the exception was raised} + */ + public final Version version() { + return version; + } + + /** + * Restores the state of a {@code StreamLimitException} from the stream + * @param in the input stream + * @throws IOException if the class of a serialized object could not be found. + * @throws ClassNotFoundException if an I/O error occurs. + * @throws InvalidObjectException if {@code version} is null. + */ + @java.io.Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if (version == null) { + throw new InvalidObjectException("version must not be null"); + } + } +} diff --git a/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java new file mode 100644 index 00000000000..eecc039e5d2 --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +import java.io.IOException; +import java.io.Serial; +import java.net.http.HttpClient.Builder; + +/** + * Thrown when the HTTP client doesn't support a particular HTTP version. + * @apiNote + * Typically, this exception may be thrown when attempting to + * {@linkplain Builder#build() build} an {@link java.net.http.HttpClient} + * configured to use {@linkplain java.net.http.HttpClient.Version#HTTP_3 + * HTTP version 3} by default, when the underlying {@link javax.net.ssl.SSLContext + * SSLContext} implementation does not meet the requirements for supporting + * the HttpClient's implementation of the underlying QUIC transport protocol. + * @since 26 + */ +public final class UnsupportedProtocolVersionException extends IOException { + + @Serial + private static final long serialVersionUID = 981344214212332893L; + + /** + * Constructs an {@code UnsupportedProtocolVersionException} with the given detail message. + * + * @param message The detail message; can be {@code null} + */ + public UnsupportedProtocolVersionException(String message) { + super(message); + } +} diff --git a/src/java.net.http/share/classes/java/net/http/package-info.java b/src/java.net.http/share/classes/java/net/http/package-info.java index 9958fd94da0..1b8395c2706 100644 --- a/src/java.net.http/share/classes/java/net/http/package-info.java +++ b/src/java.net.http/share/classes/java/net/http/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +26,7 @@ /** *

    HTTP Client and WebSocket APIs

    * - *

    Provides high-level client interfaces to HTTP (versions 1.1 and 2) and + *

    Provides high-level client interfaces to HTTP (versions 1.1, 2, and 3) and * low-level client interfaces to WebSocket. The main types defined are: * *

      @@ -37,10 +37,12 @@ *
    * *

    The protocol-specific requirements are defined in the - * Hypertext Transfer Protocol - * Version 2 (HTTP/2), the + * Hypertext Transfer Protocol + * Version 3 (HTTP/3), the + * Hypertext Transfer Protocol Version 2 (HTTP/2), the + * * Hypertext Transfer Protocol (HTTP/1.1), and - * The WebSocket Protocol. + * The WebSocket Protocol. * *

    In general, asynchronous tasks execute in either the thread invoking * the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler) @@ -66,6 +68,15 @@ *

    Unless otherwise stated, {@code null} parameter values will cause methods * of all classes in this package to throw {@code NullPointerException}. * + * @spec https://www.rfc-editor.org/info/rfc9114 + * RFC 9114: HTTP/3 + * @spec https://www.rfc-editor.org/info/rfc7540 + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + * @spec https://www.rfc-editor.org/info/rfc2616 + * RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 + * @spec https://www.rfc-editor.org/info/rfc6455 + * RFC 6455: The WebSocket Protocol + * * @since 11 */ package java.net.http; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java new file mode 100644 index 00000000000..08161bcd110 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javax.net.ssl.SNIServerName; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; + +/** + * A registry for Alternate Services advertised by server endpoints. + * There is one registry per HttpClient. + */ +public final class AltServicesRegistry { + + // id and logger for debugging purposes: the id is the same for the HttpClientImpl. + private final long id; + private final Logger debug = Utils.getDebugLogger(this::dbgString); + + // The key is the origin of the alternate service + // The value is a list of AltService records declared by the origin. + private final Map> altServices = new HashMap<>(); + // alt services which were marked invalid in context of an origin. the reason for + // them being invalid can be connection issues (for example: the alt service didn't present the + // certificate of the origin) + private final InvalidAltServices invalidAltServices = new InvalidAltServices(); + + // used while dealing with both altServices Map and the invalidAltServices Set + private final ReentrantLock registryLock = new ReentrantLock(); + + public AltServicesRegistry(long id) { + this.id = id; + } + + String dbgString() { + return "AltServicesRegistry(" + id + ")"; + } + + public static final class AltService { + // As defined in RFC-7838, section 2, formally an alternate service is a combination of + // ALPN, host and port + public record Identity(String alpn, String host, int port) { + public Identity { + Objects.requireNonNull(alpn); + Objects.requireNonNull(host); + if (port <= 0) { + throw new IllegalArgumentException("Invalid port: " + port); + } + } + + public boolean matches(AltService service) { + return equals(service.identity()); + } + + @Override + public String toString() { + return alpn + "=\"" + Origin.toAuthority(host, port) +"\""; + } + } + + private record AltServiceData(Identity id, Origin origin, Deadline deadline, + boolean persist, boolean advertised, + String authority, + boolean sameAuthorityAsOrigin) { + public String pretty() { + return "AltSvc: " + id + + "; origin=\"" + origin + "\"" + + "; deadline=" + deadline + + "; persist=" + persist + + "; advertised=" + advertised + + "; sameAuthorityAsOrigin=" + sameAuthorityAsOrigin + + ';'; + } + } + private final AltServiceData svc; + + /** + * @param id the alpn, host and port of this alternate service + * @param origin the {@link Origin} for this alternate service + * @param deadline the deadline until which this endpoint is valid + * @param persist whether that information can be persisted (we don't use this) + * @param advertised Whether or not this alt service was advertised as an alt service. + * In certain cases, an alt service is created when no origin server + * has advertised it. In those cases, this param is {@code false} + */ + private AltService(final Identity id, final Origin origin, Deadline deadline, + final boolean persist, + final boolean advertised) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + assert origin.isSecure() : "origin " + origin + " is not secure"; + deadline = deadline == null ? Deadline.MAX : deadline; + final String authority = Origin.toAuthority(id.host, id.port); + final String originAuthority = Origin.toAuthority(origin.host(), origin.port()); + // keep track of whether the authority of this alt service is same as that + // of the origin + final boolean sameAuthorityAsOrigin = authority.equals(originAuthority); + svc = new AltServiceData(id, origin, deadline, persist, advertised, + authority, sameAuthorityAsOrigin); + } + + public Identity identity() { + return svc.id; + } + + /** + * @return {@code host:port} of the alternate service + */ + public String authority() { + return svc.authority; + } + + /** + * @return {@code identity().host()} + */ + public String host() { + return svc.id.host; + } + + /** + * @return {@code identity().port()} + */ + public int port() { + return svc.id.port; + } + + public boolean isPersist() { + return svc.persist; + } + + public boolean wasAdvertised() { + return svc.advertised; + } + + public String alpn() { + return svc.id.alpn; + } + + public Origin origin() { + return svc.origin; + } + + public Deadline deadline() { + return svc.deadline; + } + + /** + * {@return true if the origin, for which this is an alternate service, has the + * same authority as this alternate service. false otherwise.} + */ + public boolean originHasSameAuthority() { + return svc.sameAuthorityAsOrigin; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AltService service)) return false; + return svc.equals(service.svc); + } + + @Override + public int hashCode() { + return svc.hashCode(); + } + + @Override + public String toString() { + return svc.pretty(); + } + + public static Optional create(final Identity id, final Origin origin, + final Deadline deadline, final boolean persist) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + if (!origin.isSecure()) { + return Optional.empty(); + } + return Optional.of(new AltService(id, origin, deadline, persist, true)); + } + + private static Optional createUnadvertised(final Logger debug, + final Identity id, final Origin origin, + final HttpConnection conn, + final Deadline deadline, final boolean persist) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + if (!origin.isSecure()) { + return Optional.empty(); + } + final List sniServerNames = AltSvcProcessor.getSNIServerNames(conn); + if (sniServerNames == null || sniServerNames.isEmpty()) { + if (debug.on()) { + debug.log("Skipping unadvertised altsvc creation of %s because connection %s" + + " didn't use SNI during connection establishment", id, conn); + } + return Optional.empty(); + } + return Optional.of(new AltService(id, origin, deadline, persist, false)); + } + + } + + // A size limited collection which keeps track of unique InvalidAltSvc instances. + // Upon reaching a pre-defined size limit, after adding newer entries, the collection + // then removes the eldest (the least recently added) entry from the collection. + // The implementation of this class is not thread safe and any concurrent access + // to instances of this class should be guarded externally. + private static final class InvalidAltServices extends LinkedHashMap { + + private static final long serialVersionUID = 2772562283544644819L; + + // we track only a reasonably small number of invalid alt services + private static final int MAX_TRACKED_INVALID_ALT_SVCS = 20; + + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > MAX_TRACKED_INVALID_ALT_SVCS; + } + + private boolean contains(final InvalidAltSvc invalidAltSvc) { + return this.containsKey(invalidAltSvc); + } + + private boolean addUnique(final InvalidAltSvc invalidAltSvc) { + if (contains(invalidAltSvc)) { + return false; + } + this.put(invalidAltSvc, null); + return true; + } + } + + // An alt-service is invalid for a particular origin + private record InvalidAltSvc(Origin origin, AltService.Identity id) { + } + + private boolean keepAltServiceFor(Origin origin, AltService svc) { + // skip invalid alt services + if (isMarkedInvalid(origin, svc.identity())) { + if (debug.on()) { + debug.log("Not registering alt-service which was previously" + + " marked invalid: " + svc); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService skipped (was previously marked invalid): " + svc); + } + return false; + } + return true; + } + + /** + * Declare a new Alternate Service endpoint for the given origin. + * + * @param origin the origin + * @param services a set of alt services for the origin + */ + public void replace(final Origin origin, final List services) { + Objects.requireNonNull(origin); + Objects.requireNonNull(services); + List added; + registryLock.lock(); + try { + // the list needs to be thread safe to ensure that we won't + // get a ConcurrentModificationException when iterating + // through the elements in list::stream(); + added = altServices.compute(origin, (key, list) -> { + Stream svcs = services.stream() + .filter(AltService.class::isInstance) // filter null + .filter((s) -> keepAltServiceFor(origin, s)); + List newList = svcs.toList(); + return newList.isEmpty() ? null : newList; + }); + } finally { + registryLock.unlock(); + } + if (debug.on()) { + debug.log("parsed services: %s", services); + debug.log("resulting services: %s", added); + } + if (Log.altsvc()) { + if (added != null) { + added.forEach((svc) -> Log.logAltSvc("AltService registry updated: {0}", svc)); + } + } + } + + // should be invoked while holding registryLock + private boolean isMarkedInvalid(final Origin origin, final AltService.Identity id) { + assert registryLock.isHeldByCurrentThread() : "Thread isn't holding registry lock"; + return this.invalidAltServices.contains(new InvalidAltSvc(origin, id)); + } + + /** + * Registers an unadvertised alt service for the given origin and the alpn. + * + * @param id The alt service identity + * @param origin The origin + * @return An {@code Optional} containing the registered {@code AltService}, + * or {@link Optional#empty()} if the service was not registered. + */ + Optional registerUnadvertised(final AltService.Identity id, + final Origin origin, + final HttpConnection conn) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + registryLock.lock(); + try { + // an unadvertised alt service is registered by an origin only after a + // successful connection has completed with that alt service. This effectively means + // that we shouldn't check our "invalid alt services" collection, since a successful + // connection against the alt service implies a valid alt service. + // Additionally, we remove it from the "invalid alt services" collection for this + // origin, if at all it was part of that collection + this.invalidAltServices.remove(new InvalidAltSvc(origin, id)); + // default max age as per AltService RFC-7838, section 3.1 is 24 hours. we use + // that same value for unadvertised alt-service(s) for an origin. + final long defaultMaxAgeInSecs = 3600 * 24; + final Deadline deadline = TimeSource.now().plusSeconds(defaultMaxAgeInSecs); + final Optional created = AltService.createUnadvertised(debug, + id, origin, conn, deadline, true); + if (created.isEmpty()) { + return Optional.empty(); + } + final AltService altSvc = created.get(); + altServices.compute(origin, (key, list) -> { + Stream old = list == null ? Stream.empty() : list.stream(); + List newList = Stream.concat(old, Stream.of(altSvc)).toList(); + return newList.isEmpty() ? null : newList; + }); + if (debug.on()) { + debug.log("Added unadvertised AltService: %s", created); + } + if (Log.altsvc()) { + Log.logAltSvc("Added unadvertised AltService: {0}", created); + } + return created; + } finally { + registryLock.unlock(); + } + } + + /** + * Clear the alternate services of the specified origin from the registry + * + * @param origin The origin whose alternate services need to be cleared + */ + public void clear(final Origin origin) { + Objects.requireNonNull(origin); + registryLock.lock(); + try { + if (Log.altsvc()) { + Log.logAltSvc("Clearing AltServices for: " + origin); + } + altServices.remove(origin); + } finally { + registryLock.unlock(); + } + } + + public void markInvalid(final AltService altService) { + Objects.requireNonNull(altService); + markInvalid(altService.origin(), altService.identity()); + } + + private void markInvalid(final Origin origin, final AltService.Identity id) { + Objects.requireNonNull(origin); + Objects.requireNonNull(id); + registryLock.lock(); + try { + // remove this alt service from the current active set of the origin + this.altServices.computeIfPresent(origin, + (key, currentActive) -> { + assert currentActive != null; // should never be null according to spec + List newList = currentActive.stream() + .filter(Predicate.not(id::matches)).toList(); + return newList.isEmpty() ? null : newList; + + }); + // additionally keep track of this as an invalid alt service, so that it cannot be + // registered again in the future. Banning is temporary. + // Banned alt services may get removed from the set at some point due to + // implementation constraints. In which case they may get registered again + // and banned again, if connecting to the endpoint fails again. + this.invalidAltServices.addUnique(new InvalidAltSvc(origin, id)); + if (debug.on()) { + debug.log("AltService marked invalid: " + id + " for origin " + origin); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService marked invalid: " + id + " for origin " + origin); + } + } finally { + registryLock.unlock(); + } + + } + + public Stream lookup(final URI uri, final String alpn) { + final Origin origin; + try { + origin = Origin.from(uri); + } catch (IllegalArgumentException iae) { + return Stream.empty(); + } + return lookup(origin, alpn); + } + + /** + * A stream of {@code AlternateService} that are available for the + * given origin and the given ALPN. + * + * @param origin the URI of the origin server + * @param alpn the ALPN of the alternate service + * @return a stream of {@code AlternateService} that are available for the + * given origin and that support the given ALPN + */ + public Stream lookup(final Origin origin, final String alpn) { + return lookup(origin, Predicate.isEqual(alpn)); + } + + public Stream lookup(final URI uri, + final Predicate alpnMatcher) { + final Origin origin; + try { + origin = Origin.from(uri); + } catch (IllegalArgumentException iae) { + return Stream.empty(); + } + return lookup(origin, alpnMatcher); + } + + private boolean isExpired(AltService service, Deadline now) { + var deadline = service.deadline(); + if (now.equals(deadline) || now.isAfter(deadline)) { + // expired, remove from the list + if (debug.on()) { + debug.log("Removing expired alt-service " + service); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService has expired: {0}", service); + } + return true; + } + return false; + } + + /** + * A stream of {@code AlternateService} that are available for the + * given origin and the given ALPN. + * + * @param origin the URI of the origin server + * @param alpnMatcher a predicate to select particular AltService(s) based on the alpn + * of the alternate service + * @return a stream of {@code AlternateService} that are available for the + * given origin and whose ALPN satisfies the {@code alpn} predicate. + */ + private Stream lookup(final Origin origin, + final Predicate alpnMatcher) { + if (debug.on()) debug.log("looking up alt-service for: %s", origin); + final List services; + registryLock.lock(); + try { + // we first drop any expired services + final Deadline now = TimeSource.now(); + services = altServices.compute(origin, (key, list) -> { + if (list == null) return null; + List newList = list.stream() + .filter((s) -> !isExpired(s, now)) + .toList(); + return newList.isEmpty() ? null : newList; + }); + } finally { + registryLock.unlock(); + } + // the order is important - since preferred service are at the head + return services == null + ? Stream.empty() + : services.stream().sequential().filter(s -> alpnMatcher.test(s.identity().alpn())); + } + + /** + * @param altService The alternate service + * {@return true if the {@code service} is known to this registry and the + * service isn't past its max age. false otherwise} + * @throws NullPointerException if {@code service} is null + */ + public boolean isActive(final AltService altService) { + Objects.requireNonNull(altService); + return isActive(altService.origin(), altService.identity()); + } + + private boolean isActive(final Origin origin, final AltService.Identity id) { + Objects.requireNonNull(origin); + Objects.requireNonNull(id); + registryLock.lock(); + try { + final List currentActive = this.altServices.get(origin); + if (currentActive == null) { + return false; + } + AltService svc = null; + for (AltService s : currentActive) { + if (s.identity().equals(id)) { + svc = s; + break; + } + } + if (svc == null) { + return false; + } + // verify that the service hasn't expired + final Deadline now = TimeSource.now(); + final Deadline deadline = svc.deadline(); + final boolean expired = now.equals(deadline) || now.isAfter(deadline); + if (expired) { + // remove from the registry + altServices.put(origin, currentActive.stream() + .filter(Predicate.not(svc::equals)).toList()); + if (debug.on()) { + debug.log("Removed expired alt-service " + svc + " for origin " + origin); + } + if (Log.altsvc()) { + Log.logAltSvc("Removed AltService: {0}", svc); + } + return false; + } + return true; + } finally { + registryLock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java new file mode 100644 index 00000000000..b172a242346 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.frame.AltSvcFrame; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; + +import static jdk.internal.net.http.Http3ClientProperties.ALTSVC_ALLOW_LOCAL_HOST_ORIGIN; +import static jdk.internal.net.http.common.Alpns.isSecureALPNName; + + +/** + * Responsible for parsing the Alt-Svc values from an Alt-Svc header and/or AltSvc HTTP/2 frame. + */ +final class AltSvcProcessor { + + private static final String HEADER = "alt-svc"; + private static final Logger debug = Utils.getDebugLogger(() -> "AltSvc"); + // a special value that we return back while parsing the header values, + // indicate that all existing alternate services for a origin need to be cleared + private static final List CLEAR_ALL_ALT_SVCS = List.of(); + // whether or not alt service can be created from "localhost" origin host + private static final boolean allowLocalHostOrigin = ALTSVC_ALLOW_LOCAL_HOST_ORIGIN; + + private static final SNIHostName LOCALHOST_SNI = new SNIHostName("localhost"); + + private record ParsedHeaderValue(String rawValue, String alpnName, String host, int port, + Map parameters) { + } + + private AltSvcProcessor() { + throw new UnsupportedOperationException("Instantiation not supported"); + } + + + /** + * Parses the alt-svc header received from origin and update + * registry with the processed values. + * + * @param response response passed on by the server + * @param client client that holds alt-svc registry + * @param request request that holds the origin details + */ + static void processAltSvcHeader(Response response, HttpClientImpl client, + HttpRequestImpl request) { + + // we don't support AltSvc from unsecure origins + if (!request.secure()) { + return; + } + if (response.statusCode == 421) { + // As per AltSvc spec (RFC-7838), section 6: + // An Alt-Svc header field in a 421 (Misdirected Request) response MUST be ignored. + return; + } + final var altSvcHeaderVal = response.headers().firstValue(HEADER); + if (altSvcHeaderVal.isEmpty()) { + return; + } + if (debug.on()) { + debug.log("Processing alt-svc header"); + } + final HttpConnection conn = response.exchange.exchImpl.connection(); + final List sniServerNames = getSNIServerNames(conn); + if (sniServerNames.isEmpty()) { + // we don't trust the alt-svc advertisement if the connection over which it + // was advertised didn't use SNI during TLS handshake while establishing the connection + if (debug.on()) { + debug.log("ignoring alt-svc header because connection %s didn't use SNI during" + + " connection establishment", conn); + } + return; + } + final Origin origin; + try { + origin = Origin.from(request.uri()); + } catch (IllegalArgumentException iae) { + if (debug.on()) { + debug.log("ignoring alt-svc header due to: " + iae); + } + // ignore the alt-svc + return; + } + String altSvcValue = altSvcHeaderVal.get(); + processValueAndUpdateRegistry(client, origin, altSvcValue); + } + + static void processAltSvcFrame(final int streamId, + final AltSvcFrame frame, + final HttpConnection conn, + final HttpClientImpl client) { + final String value = frame.getAltSvcValue(); + if (value == null || value.isBlank()) { + return; + } + if (!conn.isSecure()) { + // don't support alt svc from unsecure origins + return; + } + final List sniServerNames = getSNIServerNames(conn); + if (sniServerNames.isEmpty()) { + // we don't trust the alt-svc advertisement if the connection over which it + // was advertised didn't use SNI during TLS handshake while establishing the connection + if (debug.on()) { + debug.log("ignoring altSvc frame because connection %s didn't use SNI during" + + " connection establishment", conn); + } + return; + } + debug.log("processing AltSvcFrame %s", value); + final Origin origin; + if (streamId == 0) { + // section 4, RFC-7838 - alt-svc frame on stream 0 with empty (zero length) origin + // is invalid and MUST be ignored + if (frame.getOrigin().isBlank()) { + // invalid frame, ignore it + debug.log("Ignoring invalid alt-svc frame on stream 0 of " + conn); + return; + } + // parse origin from frame.getOrigin() string which is in ASCII + // serialized form of an origin (defined in section 6.2 of RFC-6454) + final Origin parsedOrigin; + try { + parsedOrigin = Origin.fromASCIISerializedForm(frame.getOrigin()); + } catch (IllegalArgumentException iae) { + // invalid origin value, ignore the frame + debug.log("origin couldn't be parsed, ignoring invalid alt-svc frame" + + " on stream " + streamId + " of " + conn); + return; + } + // currently we do not allow an alt service to be advertised for a different origin. + // if the origin advertised in the alt-svc frame doesn't match the origin of the + // connection, then we ignore it. the RFC allows us to do that: + // RFC-7838, section 4: + // An ALTSVC frame from a server to a client on stream 0 indicates that + // the conveyed alternative service is associated with the origin + // contained in the Origin field of the frame. An association with an + // origin that the client does not consider authoritative for the + // current connection MUST be ignored. + if (!parsedOrigin.equals(conn.getOriginServer())) { + debug.log("ignoring alt-svc frame on stream 0 for origin: " + parsedOrigin + + " received on connection of origin: " + conn.getOriginServer()); + return; + } + origin = parsedOrigin; + } else { + // (section 4, RFC-7838) - for non-zero stream id, the alt-svc is for the origin of + // the stream. Additionally, an ALTSVC frame on a stream other than stream 0 containing + // non-empty "Origin" information is invalid and MUST be ignored. + if (!frame.getOrigin().isEmpty()) { + // invalid frame, ignore it + debug.log("non-empty origin in alt-svc frame on stream " + streamId + " of " + + conn + ", ignoring alt-svc frame"); + return; + } + origin = conn.getOriginServer(); + assert origin != null : "origin server is null on connection: " + conn; + } + processValueAndUpdateRegistry(client, origin, value); + } + + private static void processValueAndUpdateRegistry(HttpClientImpl client, + Origin origin, + String altSvcValue) { + final List altServices = processHeaderValue(origin, altSvcValue); + // intentional identity check + if (altServices == CLEAR_ALL_ALT_SVCS) { + // clear all existing alt services for this origin + debug.log("clearing AltServiceRegistry for " + origin); + client.registry().clear(origin); + return; + } + debug.log("AltServices: %s", altServices); + if (altServices.isEmpty()) { + return; + } + // AltService RFC-7838, section 3.1 states: + // + // When an Alt-Svc response header field is received from an origin, its + // value invalidates and replaces all cached alternative services for + // that origin. + client.registry().replace(origin, altServices); + } + + static List getSNIServerNames(final HttpConnection conn) { + final List sniServerNames = conn.getSNIServerNames(); + if (sniServerNames != null && !sniServerNames.isEmpty()) { + return sniServerNames; + } + // no SNI server name(s) were used when establishing this connection. check if + // this connection is to a loopback address and if it is then see if a configuration + // has been set to allow alt services advertised by loopback addresses to be trusted/accepted. + // if such a configuration has been set, then we return a SNIHostName for "localhost" + final InetSocketAddress addr = conn.address(); + final boolean isLoopbackAddr = addr.isUnresolved() + ? false + : conn.address.getAddress().isLoopbackAddress(); + if (!isLoopbackAddr) { + return List.of(); // no SNI server name(s) used for this connection + } + if (!allowLocalHostOrigin) { + // this is a connection to a loopback address, with no SNI server name(s) used + // during TLS handshake and the configuration doesn't allow accepting/trusting + // alt services from loopback address, so we return no SNI server name(s) for this + // connection + return List.of(); + } + // at this point, we have identified this as a loopback address and the configuration + // has been set to accept/trust alt services from loopback address, so we return a + // SNIHostname corresponding to "localhost" + return List.of(LOCALHOST_SNI); + } + + // Here are five examples of values for the Alt-Svc header: + // String svc1 = """foo=":443"; ma=2592000; persist=1""" + // String svc2 = """h3="localhost:5678""""; + // String svc3 = """bar3=":446"; ma=2592000; persist=1"""; + // String svc4 = """h3-34=":5678"; ma=2592000; persist=1"""; + // String svc5 = "%s, %s, %s, %s".formatted(svc1, svc2, svc3, svc4); + // The last one (svc5) should result in two services being registered: + // AltService[origin=https://localhost:64077/, alpn=h3, endpoint=localhost/127.0.0.1:5678, + // deadline=2021-03-13T01:41:01.369488Z, persist=false] + // AltService[origin=https://localhost:64077/, alpn=h3-34, endpoint=localhost/127.0.0.1:5678, + // deadline=2021-04-11T01:41:01.369912Z, persist=true] + private static List processHeaderValue(final Origin origin, + final String headerValue) { + final List altServices = new ArrayList<>(); + // multiple alternate services can be specified with comma as a delimiter + final var altSvcs = headerValue.split(","); + for (var altSvc : altSvcs) { + altSvc = altSvc.trim(); + + // each value is expected to be of the following form, as noted in RFC-7838, section 3 + // Alt-Svc = clear / 1#alt-value + // clear = %s"clear"; "clear", case-sensitive + // alt-value = alternative *( OWS ";" OWS parameter ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol name + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // parameter = token "=" ( token / quoted-string ) + + // As per the spec, the value "clear" is expected to be case-sensitive + if (altSvc.equals("clear")) { + return CLEAR_ALL_ALT_SVCS; + } + final ParsedHeaderValue parsed = parseAltValue(origin, altSvc); + if (parsed == null) { + // this implies the alt-svc header value couldn't be parsed and thus is malformed. + // we skip such header values. + debug.log("skipping %s", altSvc); + continue; + } + final var deadline = getValidTill(parsed.parameters().get("ma")); + final var persist = getPersist(parsed.parameters().get("persist")); + final AltService.Identity altSvcId = new AltService.Identity(parsed.alpnName(), + parsed.host(), parsed.port()); + AltService.create(altSvcId, origin, deadline, persist) + .ifPresent((altsvc) -> { + altServices.add(altsvc); + if (Log.altsvc()) { + final var s = altsvc; + Log.logAltSvc("Created AltService: {0}", s); + } else if (debug.on()) { + debug.log("Created AltService for id=%s, origin=%s%n", altSvcId, origin); + } + }); + } + return altServices; + } + + private static ParsedHeaderValue parseAltValue(final Origin origin, final String altValue) { + // header value is expected to be of the following form, as noted in RFC-7838, section 3 + // Alt-Svc = clear / 1#alt-value + // clear = %s"clear"; "clear", case-sensitive + // alt-value = alternative *( OWS ";" OWS parameter ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol name + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // parameter = token "=" ( token / quoted-string ) + + // find the = sign that separates the protocol-id and alt-authority + debug.log("parsing %s", altValue); + final int alternativeDelimIndex = altValue.indexOf("="); + if (alternativeDelimIndex == -1 || alternativeDelimIndex == altValue.length() - 1) { + // not a valid alt value + debug.log("no \"=\" character in %s", altValue); + return null; + } + // key is always the protocol-id. example, in 'h3="localhost:5678"; ma=23232; persist=1' + // "h3" acts as the key with '"localhost:5678"; ma=23232; persist=1' as the value + final String protocolId = altValue.substring(0, alternativeDelimIndex); + // the protocol-id can be percent encoded as per the spec, so we decode it to get the alpn name + final var alpnName = decodePotentialPercentEncoded(protocolId); + debug.log("alpn is %s in %s", alpnName, altValue); + if (!isSecureALPNName(alpnName)) { + // no reasonable assurance that the alternate service will be under the control + // of the origin (section 2.1, RFC-7838) + debug.log("alpn %s is not secure, skipping", alpnName); + return null; + } + String remaining = altValue.substring(alternativeDelimIndex + 1); + // now parse alt-authority + if (!remaining.startsWith("\"") || remaining.length() == 1) { + // we expect a quoted string for alt-authority + debug.log("no quoted authority in %s", altValue); + return null; + } + remaining = remaining.substring(1); // skip the starting double quote + final int nextDoubleQuoteIndex = remaining.indexOf("\""); + if (nextDoubleQuoteIndex == -1) { + // malformed value + debug.log("missing closing quote in %s", altValue); + return null; + } + final String altAuthority = remaining.substring(0, nextDoubleQuoteIndex); + final HostPort hostPort = getHostPort(origin, altAuthority); + if (hostPort == null) return null; // host port could not be parsed + if (nextDoubleQuoteIndex == remaining.length() - 1) { + // there's nothing more left to parse + return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), Map.of()); + } + // parse the semicolon delimited parameters out of the rest of the remaining string + remaining = remaining.substring(nextDoubleQuoteIndex + 1); + final Map parameters = extractParameters(remaining); + return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), parameters); + } + + private static String decodePotentialPercentEncoded(final String val) { + if (!val.contains("%")) { + return val; + } + // TODO: impl this + // In practice this method is only used for the ALPN. + // We only support h3 for now, so we do not need to + // decode percents: anything else but h3 will eventually be ignored. + return val; + } + + private static Map extractParameters(final String val) { + // As per the spec, parameters take the form of: + // *( OWS ";" OWS parameter ) + // ... + // parameter = token "=" ( token / quoted-string ) + // + // where * represents "any number of" and OWS means "optional whitespace" + + final var tokenizer = new StringTokenizer(val, ";"); + if (!tokenizer.hasMoreTokens()) { + return Map.of(); + } + Map parameters = null; + while (tokenizer.hasMoreTokens()) { + final var parameter = tokenizer.nextToken().trim(); + if (parameter.isEmpty()) { + continue; + } + final var equalSignIndex = parameter.indexOf('='); + if (equalSignIndex == -1 || equalSignIndex == parameter.length() - 1) { + // a parameter is expected to have a "=" delimiter which separates a key and a value. + // we skip parameters which don't conform to that rule + continue; + } + final var paramKey = parameter.substring(0, equalSignIndex); + final var paramValue = parameter.substring(equalSignIndex + 1); + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.put(paramKey, paramValue); + } + if (parameters == null) { + return Map.of(); + } + return Collections.unmodifiableMap(parameters); + } + + private record HostPort(String host, int port) {} + + private static HostPort getHostPort(Origin origin, String altAuthority) { + // The AltService spec defines an alt-authority as follows: + // + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // + // When this method is called the passed altAuthority is already stripped of the leading and trailing + // double-quotes. The value will this be of the form [uri-host]:port where uri-host is optional. + String host; int port; + try { + // Use URI to do the parsing, with a special case for optional host + URI uri = new URI("http://" + altAuthority + "/"); + host = uri.getHost(); + port = uri.getPort(); + if (host == null && port == -1) { + var auth = uri.getRawAuthority(); + if (auth.isEmpty()) return null; + if (auth.charAt(0) == ':') { + uri = new URI("http://x" + altAuthority + "/"); + if ("x".equals(uri.getHost())) { + port = uri.getPort(); + } + } + } + if (port == -1) { + debug.log("Can't parse authority: " + altAuthority); + return null; + } + String hostport; + if (host == null || host.isEmpty()) { + hostport = ":" + port; + host = origin.host(); + } else { + hostport = host + ":" + port; + } + // reject anything unexpected. altAuthority should match hostport + if (!hostport.equals(altAuthority)) { + debug.log("Authority \"%s\" doesn't match host:port \"%s\"", + altAuthority, hostport); + return null; + } + } catch (URISyntaxException x) { + debug.log("Failed to parse authority: %s - %s", + altAuthority, x); + return null; + } + return new HostPort(host, port); + } + + private static Deadline getValidTill(final String maxAge) { + // There's a detailed algorithm in RFC-7234 section 4.2.3, for calculating the age. This + // RFC section is referenced from the alternate service RFC-7838 section 3.1. + // For now though, we use "now" as the instant against which the age will be applied. + final Deadline responseGenerationInstant = TimeSource.now(); + // default max age as per AltService RFC-7838, section 3.1 is 24 hours + final long defaultMaxAgeInSecs = 3600 * 24; + if (maxAge == null) { + return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs); + } + try { + final long seconds = Long.parseLong(maxAge); + // negative values aren't allowed for max-age as per RFC-7234, section 1.2.1 + return seconds < 0 ? responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs) + : responseGenerationInstant.plusSeconds(seconds); + } catch (NumberFormatException nfe) { + return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs); + } + } + + private static boolean getPersist(final String persist) { + // AltService RFC-7838, section 3.1, states: + // + // This specification only defines a single value for "persist". + // Clients MUST ignore "persist" parameters with values other than "1". + // + return "1".equals(persist); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java new file mode 100644 index 00000000000..bb74ac3d6e9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.util.Iterator; + +/** + * An {@link Iterable} clone supporting checked exceptions. + * + * @param the type of elements returned by the produced iterators + */ +@FunctionalInterface +interface CheckedIterable { + + /** + * {@return an {@linkplain CheckedIterator iterator} over elements of type {@code E}} + */ + CheckedIterator iterator() throws Exception; + + static CheckedIterable fromIterable(Iterable iterable) { + return () -> { + Iterator iterator = iterable.iterator(); + return CheckedIterator.fromIterator(iterator); + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java new file mode 100644 index 00000000000..42e8e2b6c87 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An {@link Iterator} clone supporting checked exceptions. + * + * @param the type of elements returned by this iterator + */ +interface CheckedIterator { + + /** + * {@return {@code true} if the iteration has more elements} + * @throws Exception if operation fails + */ + boolean hasNext() throws Exception; + + /** + * {@return the next element in the iteration} + * + * @throws NoSuchElementException if the iteration has no more elements + * @throws Exception if operation fails + */ + E next() throws Exception; + + static CheckedIterator fromIterator(Iterator iterator) { + return new CheckedIterator<>() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public E next() { + return iterator.next(); + } + + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java index 1ee54ed2bef..c50a4922e80 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java @@ -27,6 +27,7 @@ package jdk.internal.net.http; import java.io.IOException; import java.net.ProtocolException; +import java.net.http.HttpClient.Version; import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -62,6 +63,7 @@ final class Exchange { volatile ExchangeImpl exchImpl; volatile CompletableFuture> exchangeCF; volatile CompletableFuture bodyIgnored; + volatile boolean streamLimitReached; // used to record possible cancellation raised before the exchImpl // has been established. @@ -74,11 +76,18 @@ final class Exchange { final String dbgTag; // Keeps track of the underlying connection when establishing an HTTP/2 - // exchange so that it can be aborted/timed out mid setup. + // or HTTP/3 exchange so that it can be aborted/timed out mid-setup. final ConnectionAborter connectionAborter = new ConnectionAborter(); final AtomicInteger nonFinalResponses = new AtomicInteger(); + // This will be set to true only when it is guaranteed that the server hasn't processed + // the request. Typically, this happens when the server explicitly states (through a GOAWAY frame + // or a relevant error code in reset frame) that the corresponding stream (id) wasn't processed. + // However, there can be cases where the client is certain that the request wasn't sent + // to the server (and thus not processed). In such cases, the client can set this to true. + private volatile boolean unprocessedByPeer; + Exchange(HttpRequestImpl request, MultiExchange multi) { this.request = request; this.upgrading = false; @@ -110,9 +119,13 @@ final class Exchange { } // Keeps track of the underlying connection when establishing an HTTP/2 - // exchange so that it can be aborted/timed out mid setup. - static final class ConnectionAborter { + // or HTTP/3 exchange so that it can be aborted/timed out mid setup. + final class ConnectionAborter { + // In case of HTTP/3 requests we may have + // two connections in parallel: a regular TCP connection + // and a QUIC connection. private volatile HttpConnection connection; + private volatile HttpQuicConnection quicConnection; private volatile boolean closeRequested; private volatile Throwable cause; @@ -123,10 +136,11 @@ final class Exchange { // closed closeRequested = this.closeRequested; if (!closeRequested) { - this.connection = connection; - } else { - // assert this.connection == null - this.closeRequested = false; + if (connection instanceof HttpQuicConnection quicConnection) { + this.quicConnection = quicConnection; + } else { + this.connection = connection; + } } } if (closeRequested) closeConnection(connection, cause); @@ -134,6 +148,7 @@ final class Exchange { void closeConnection(Throwable error) { HttpConnection connection; + HttpQuicConnection quicConnection; Throwable cause; synchronized (this) { cause = this.cause; @@ -141,39 +156,64 @@ final class Exchange { cause = error; } connection = this.connection; - if (connection == null) { + quicConnection = this.quicConnection; + if (connection == null || quicConnection == null) { closeRequested = true; this.cause = cause; } else { + this.quicConnection = null; this.connection = null; this.cause = null; } } closeConnection(connection, cause); + closeConnection(quicConnection, cause); } + // Called by HTTP/2 after an upgrade. + // There is no upgrade for HTTP/3 HttpConnection disable() { HttpConnection connection; synchronized (this) { connection = this.connection; this.connection = null; + this.quicConnection = null; this.closeRequested = false; this.cause = null; } return connection; } - private static void closeConnection(HttpConnection connection, Throwable cause) { - if (connection != null) { - try { - connection.close(cause); - } catch (Throwable t) { - // ignore + void clear(HttpConnection connection) { + synchronized (this) { + var c = this.connection; + if (connection == c) this.connection = null; + var qc = this.quicConnection; + if (connection == qc) this.quicConnection = null; + } + } + + private void closeConnection(HttpConnection connection, Throwable cause) { + if (connection == null) { + return; + } + try { + connection.close(cause); + } catch (Throwable t) { + // ignore + if (debug.on()) { + debug.log("ignoring exception that occurred during closing of connection: " + + connection, t); } } } } + // true if previous attempt resulted in streamLimitReached + public boolean hasReachedStreamLimit() { return streamLimitReached; } + // can be used to set or clear streamLimitReached (for instance clear it after retrying) + void streamLimitReached(boolean streamLimitReached) { this.streamLimitReached = streamLimitReached; } + // Called for 204 response - when no body is permitted // This is actually only needed for HTTP/1.1 in order // to return the connection to the pool (or close it) @@ -253,7 +293,7 @@ final class Exchange { impl.cancel(cause); } else { // abort/close the connection if setting up the exchange. This can - // be important when setting up HTTP/2 + // be important when setting up HTTP/2 or HTTP/3 closeReason = failed.get(); if (closeReason != null) { connectionAborter.closeConnection(closeReason); @@ -283,6 +323,9 @@ final class Exchange { cf = exchangeCF; } } + if (multi.requestCancelled() && impl != null && cause == null) { + cause = new IOException("Request cancelled"); + } if (cause == null) return; if (impl != null) { // The exception is raised by propagating it to the impl. @@ -314,7 +357,7 @@ final class Exchange { // if upgraded, we don't close the connection. // cancelling will be handled by the HTTP/2 exchange // in its own time. - if (!upgraded) { + if (!upgraded && !(connection instanceof HttpQuicConnection)) { t = getCancelCause(); if (t == null) t = new IOException("Request cancelled"); if (debug.on()) debug.log("exchange cancelled during connect: " + t); @@ -350,8 +393,8 @@ final class Exchange { private CompletableFuture> establishExchange(HttpConnection connection) { if (debug.on()) { - debug.log("establishing exchange for %s,%n\t proxy=%s", - request, request.proxy()); + debug.log("establishing exchange for %s #%s,%n\t proxy=%s", + request, multi.id, request.proxy()); } // check if we have been cancelled first. Throwable t = getCancelCause(); @@ -364,7 +407,17 @@ final class Exchange { } CompletableFuture> cf, res; - cf = ExchangeImpl.get(this, connection); + + cf = ExchangeImpl.get(this, connection) + // set exchImpl and call checkCancelled to make sure exchImpl + // gets cancelled even if the exchangeCf was completed exceptionally + // before the CF returned by ExchangeImpl.get completed. This deals + // with issues when the request is cancelled while the exchange impl + // is being created. + .thenApply((eimpl) -> { + synchronized (Exchange.this) {exchImpl = eimpl;} + checkCancelled(); return eimpl; + }).copy(); // We should probably use a VarHandle to get/set exchangeCF // instead - as we need CAS semantics. synchronized (this) { exchangeCF = cf; }; @@ -390,7 +443,7 @@ final class Exchange { } // Completed HttpResponse will be null if response succeeded - // will be a non null responseAsync if expect continue returns an error + // will be a non-null responseAsync if expect continue returns an error public CompletableFuture responseAsync() { return responseAsyncImpl(null); @@ -715,4 +768,13 @@ final class Exchange { String dbgString() { return dbgTag; } + + final boolean isUnprocessedByPeer() { + return this.unprocessedByPeer; + } + + // Marks the exchange as unprocessed by the peer + final void markUnprocessedByPeer() { + this.unprocessedByPeer = true; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java index f393b021cd4..74600e78557 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,17 +26,26 @@ package jdk.internal.net.http; import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.ResponseInfo; +import java.net.http.UnsupportedProtocolVersionException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.Supplier; +import jdk.internal.net.http.Http2Connection.ALPNException; import jdk.internal.net.http.common.HttpBodySubscriberWrapper; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.MinimalFuture; import jdk.internal.net.http.common.Utils; import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; /** * Splits request so that headers and body can be sent separately with optional @@ -60,10 +69,6 @@ abstract class ExchangeImpl { private volatile boolean expectTimeoutRaised; - // this will be set to true only when the peer explicitly states (through a GOAWAY frame or - // a relevant error code in reset frame) that the corresponding stream (id) wasn't processed - private volatile boolean unprocessedByPeer; - ExchangeImpl(Exchange e) { // e == null means a http/2 pushed stream this.exchange = e; @@ -98,23 +103,414 @@ abstract class ExchangeImpl { static CompletableFuture> get(Exchange exchange, HttpConnection connection) { - if (exchange.version() == HTTP_1_1) { + HttpRequestImpl request = exchange.request(); + var version = exchange.version(); + if (version == HTTP_1_1 || request.isWebSocket()) { if (debug.on()) debug.log("get: HTTP/1.1: new Http1Exchange"); return createHttp1Exchange(exchange, connection); - } else { - Http2ClientImpl c2 = exchange.client().client2(); // #### improve - HttpRequestImpl request = exchange.request(); - CompletableFuture c2f = c2.getConnectionFor(request, exchange); + } else if (!request.secure() && request.isHttp3Only(version)) { + assert version == HTTP_3; + assert !request.isWebSocket(); if (debug.on()) - debug.log("get: Trying to get HTTP/2 connection"); - // local variable required here; see JDK-8223553 - CompletableFuture>> fxi = - c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection)); - return fxi.thenCompose(x->x); + debug.log("get: HTTP/3: HTTP/3 is not supported on plain connections"); + return MinimalFuture.failedFuture( + new UnsupportedProtocolVersionException( + "HTTP/3 is not supported on plain connections")); + } else if (version == HTTP_2 || isTCP(connection) || !request.secure()) { + assert !request.isWebSocket(); + return attemptHttp2Exchange(exchange, connection); + } else { + assert request.secure(); + assert version == HTTP_3; + assert !request.isWebSocket(); + return attemptHttp3Exchange(exchange, connection); } } + private static boolean isTCP(HttpConnection connection) { + if (connection instanceof HttpQuicConnection) return false; + if (connection == null) return false; + // if it's not an HttpQuicConnection and it's not null it's + // a TCP connection + return true; + } + + private static CompletableFuture> + attemptHttp2Exchange(Exchange exchange, HttpConnection connection) { + HttpRequestImpl request = exchange.request(); + Http2ClientImpl c2 = exchange.client().client2(); // #### improve + CompletableFuture c2f = c2.getConnectionFor(request, exchange); + if (debug.on()) + debug.log("get: Trying to get HTTP/2 connection"); + // local variable required here; see JDK-8223553 + CompletableFuture>> fxi = + c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection)); + return fxi.thenCompose(x -> x); + } + + private static CompletableFuture> + attemptHttp3Exchange(Exchange exchange, HttpConnection connection) { + HttpRequestImpl request = exchange.request(); + var exchvers = exchange.version(); + assert request.secure() : request.uri() + " is not secure"; + assert exchvers == HTTP_3 : "expected HTTP/3, got " + exchvers; + // when we reach here, it's guaranteed that the client supports HTTP3 + assert exchange.client().client3().isPresent() : "HTTP3 isn't supported by the client"; + var client3 = exchange.client().client3().get(); + CompletableFuture c3f; + Supplier> c2fs; + var config = request.http3Discovery(); + + if (debug.on()) { + debug.log("get: Trying to get HTTP/3 connection; config is %s", config); + } + // The algorithm here depends on whether HTTP/3 is specified on + // the request itself, or on the HttpClient. + // In both cases, we may attempt a direct HTTP/3 connection if + // we don't have an H3 endpoint registered in the AltServicesRegistry. + // However, if HTTP/3 is not specified explicitly on the request, + // we will start both an HTTP/2 and an HTTP/3 connection at the + // same time, and use the one that complete first. If HTTP/3 is + // specified on the request, we will give priority to HTTP/3 ond + // only start the HTTP/2 connection if the HTTP/3 connection fails, + // or doesn't succeed in the imparted timeout. The timeout can be + // specified with the property "jdk.httpclient.http3.maxDirectConnectionTimeout". + // If unspecified it defaults to 2750ms. + // + // Because the HTTP/2 connection may start as soon as we create the + // CompletableFuture returned by the Http2Client, + // we are using a Supplier> to + // set up the call chain that would start the HTTP/2 connection. + try { + // first look to see if we already have an HTTP/3 connection in + // the pool. If we find one, we're almost done! We won't need + // to start any HTTP/2 connection. + Http3Connection pooled = client3.findPooledConnectionFor(request, exchange); + if (pooled != null) { + c3f = MinimalFuture.completedFuture(pooled); + c2fs = null; + } else { + if (debug.on()) + debug.log("get: no HTTP/3 pooled connection found"); + // possibly start an HTTP/3 connection + boolean mayAttemptDirectConnection = client3.mayAttemptDirectConnection(request); + c3f = client3.getConnectionFor(request, exchange); + if ((!c3f.isDone() || c3f.isCompletedExceptionally()) && mayAttemptDirectConnection) { + // We don't know if the server supports HTTP/3. + // happy eyeball: prepare to try both HTTP/3 and HTTP/2 and + // to use the first that succeeds + if (config != Http3DiscoveryMode.HTTP_3_URI_ONLY) { + if (debug.on()) { + debug.log("get: trying with both HTTP/3 and HTTP/2"); + } + Http2ClientImpl client2 = exchange.client().client2(); + c2fs = () -> client2.getConnectionFor(request, exchange); + } else { + if (debug.on()) { + debug.log("get: trying with HTTP/3 only"); + } + c2fs = null; + } + } else { + // We have a completed Http3Connection future. + // No need to attempt direct HTTP/3 connection. + c2fs = null; + } + } + } catch (IOException io) { + return MinimalFuture.failedFuture(io); + } + if (c2fs == null) { + // Do not attempt a happy eyeball: go the normal route to + // attempt an HTTP/3 connection + // local variable required here; see JDK-8223553 + if (debug.on()) debug.log("No HTTP/3 eyeball needed"); + CompletableFuture>> fxi = + c3f.handle((h3c, t) -> createExchangeImpl(h3c, t, exchange, connection)); + return fxi.thenCompose(x->x); + } else if (request.version().orElse(null) == HTTP_3) { + // explicit request to use HTTP/3, only use HTTP/2 if HTTP/3 fails, but + // still start both connections in parallel. HttpQuicConnection will + // attempt a direct connection. Because we register + // firstToComplete as a dependent action of c3f we will actually + // only use HTTP/2 (or HTTP/1.1) if HTTP/3 failed + CompletableFuture>> fxi = + c3f.handle((h3c, e) -> firstToComplete(exchange, connection, c2fs, c3f)); + if (debug.on()) { + debug.log("Explicit HTTP/3 request: " + + "attempt HTTP/3 first, then default to HTTP/2"); + } + return fxi.thenCompose(x->x); + } + if (debug.on()) { + debug.log("Attempt HTTP/3 and HTTP/2 in parallel, use the first that connects"); + } + // default client version is HTTP/3 - request version is not set. + // so try HTTP/3 + HTTP/2 in parallel and take the first that completes. + return firstToComplete(exchange, connection, c2fs, c3f); + } + + // Use the first connection that successfully completes. + // This is a bit hairy because HTTP/2 may be downgraded to HTTP/1 if the server + // doesn't support HTTP/2. In which case the connection attempt will succeed but + // c2f will be completed with a ALPNException. + private static CompletableFuture> firstToComplete( + Exchange exchange, + HttpConnection connection, + Supplier> c2fs, + CompletableFuture c3f) { + if (debug.on()) { + debug.log("firstToComplete(connection=%s)", connection); + debug.log("Will use the first connection that succeeds from HTTP/2 or HTTP/3"); + } + assert connection == null : "should not come here if connection is not null: " + connection; + + // Set up a completable future (cf) that will complete + // when the first HTTP/3 or HTTP/2 connection result is + // available. Error cases (when the result is exceptional) + // is handled in a dependent action of cf later below + final CompletableFuture cf; + // c3f is used for HTTP/3, c2f for HTTP/2 + final CompletableFuture c2f; + if (c3f.isDone()) { + // We already have a result for HTTP/3, consider that first; + // There's no need to start HTTP/2 yet if the result is successful. + c2f = null; + cf = c3f; + } else { + // No result for HTTP/3 yet, start HTTP/2 now and wait for the + // first that completes. + c2f = c2fs.get(); + cf = CompletableFuture.anyOf(c2f, c3f); + } + + CompletableFuture>> cfxi = cf.handle((r, t) -> { + if (debug.on()) { + debug.log("Checking which from HTTP/2 or HTTP/3 succeeded first"); + } + CompletableFuture> res; + // first check if c3f is completed successfully + if (c3f.isDone()) { + Http3Connection h3c = c3f.exceptionally((e) -> null).resultNow(); + if (h3c != null) { + // HTTP/3 success! Use HTTP/3 + if (debug.on()) { + debug.log("HTTP/3 connect completed first, using HTTP/3"); + } + res = createExchangeImpl(h3c, null, exchange, connection); + if (c2f != null) c2f.thenApply(c -> { + if (c != null) { + c.abandonStream(); + } + return c; + }); + } else { + // HTTP/3 failed! Use HTTP/2 + if (debug.on()) { + debug.log("HTTP/3 connect completed unsuccessfully," + + " either with null or with exception - waiting for HTTP/2"); + c3f.handle((r3, t3) -> { + debug.log("\tcf3: result=%s, throwable=%s", + r3, Utils.getCompletionCause(t3)); + return r3; + }).exceptionally((e) -> null).join(); + } + // c2f may be null here in the case where c3f was already completed + // when firstToComplete was called. + var h2cf = c2f == null ? c2fs.get() : c2f; + // local variable required here; see JDK-8223553 + CompletableFuture>> fxi = h2cf + .handle((h2c, e) -> createExchangeImpl(h2c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } + } else if (c2f != null && c2f.isDone()) { + Http2Connection h2c = c2f.exceptionally((e) -> null).resultNow(); + if (h2c != null) { + // HTTP/2 succeeded first! Use it. + if (debug.on()) { + debug.log("HTTP/2 connect completed first, using HTTP/2"); + } + res = createExchangeImpl(h2c, null, exchange, connection); + } else if (exchange.multi.requestCancelled()) { + // special case for when the exchange is cancelled + if (debug.on()) { + debug.log("HTTP/2 connect completed unsuccessfully, but request cancelled"); + } + CompletableFuture>> fxi = c2f + .handle((c, e) -> createExchangeImpl(c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } else { + if (debug.on()) { + debug.log("HTTP/2 connect completed unsuccessfully," + + " either with null or with exception"); + c2f.handle((r2, t2) -> { + debug.log("\tcf2: result=%s, throwable=%s", + r2, Utils.getCompletionCause(t2)); + return r2; + }).exceptionally((e) -> null).join(); + } + + // Now is the more complex stuff. + // HTTP/2 could have failed in the ALPN, but we still + // created a valid TLS connection to the server => default + // to HTTP/1.1 over TLS + HttpConnection http1Connection = null; + if (c2f.isCompletedExceptionally() && !c2f.isCancelled()) { + Throwable cause = Utils.getCompletionCause(c2f.exceptionNow()); + if (cause instanceof ALPNException alpn) { + debug.log("HTTP/2 downgraded to HTTP/1.1 - use HTTP/1.1"); + http1Connection = alpn.getConnection(); + } + } + if (http1Connection != null) { + if (debug.on()) { + debug.log("HTTP/1.1 connect completed first, using HTTP/1.1"); + } + // ALPN failed - but we have a valid HTTP/1.1 connection + // to the server: use that. + res = createHttp1Exchange(exchange, http1Connection); + } else { + if (c2f.isCompletedExceptionally()) { + // Wait for HTTP/3 to complete, potentially fallback to + // HTTP/1.1 + // local variable required here; see JDK-8223553 + debug.log("HTTP/2 completed with exception, wait for HTTP/3, " + + "possibly fallback to HTTP/1.1"); + CompletableFuture>> fxi = c3f + .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } else { + // + // r2 == null && t2 == null - which means we know the + // server doesn't support h2, and we probably already + // have an HTTP/1.1 connection to it + // + // If an HTTP/1.1 connection is available use it. + // Otherwise, wait for the HTTP/3 to complete, potentially + // fallback to HTTP/1.1 + HttpRequestImpl request = exchange.request(); + InetSocketAddress proxy = Utils.resolveAddress(request.proxy()); + InetSocketAddress addr = request.getAddress(); + ConnectionPool pool = exchange.client().connectionPool(); + // if we have an HTTP/1.1 connection in the pool, use that. + http1Connection = pool.getConnection(true, addr, proxy); + if (http1Connection != null && http1Connection.isOpen()) { + debug.log("Server doesn't support HTTP/2, " + + "but we have an HTTP/1.1 connection in the pool"); + debug.log("Using HTTP/1.1"); + res = createHttp1Exchange(exchange, http1Connection); + } else { + // we don't have anything ready to use in the pool: + // wait for http/3 to complete, possibly falling back + // to HTTP/1.1 + debug.log("Server doesn't support HTTP/2, " + + "and we do not have an HTTP/1.1 connection"); + debug.log("Waiting for HTTP/3, possibly fallback to HTTP/1.1"); + CompletableFuture>> fxi = c3f + .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } + } + } + } + } else { + assert c2f != null; + Throwable failed = t != null ? t : new InternalError("cf1 or cf2 should have completed"); + res = MinimalFuture.failedFuture(failed); + } + return res; + }); + return cfxi.thenCompose(x -> x); + } + + private static CompletableFuture> + fallbackToHttp1OnTimeout(Http3Connection c, + Throwable t, + Exchange exchange, + HttpConnection connection) { + if (t != null) { + Throwable cause = Utils.getCompletionCause(t); + if (cause instanceof HttpConnectTimeoutException) { + // when we reach here we already tried with HTTP/2, + // and we most likely have an HTTP/1.1 connection in + // the idle pool. So fallback to that. + if (debug.on()) { + debug.log("HTTP/3 connection timed out: fall back to HTTP/1.1"); + } + return createHttp1Exchange(exchange, null); + } + } + return createExchangeImpl(c, t, exchange, connection); + } + + + + // Creates an HTTP/3 exchange, possibly downgrading to HTTP/2 + private static CompletableFuture> + createExchangeImpl(Http3Connection c, + Throwable t, + Exchange exchange, + HttpConnection connection) { + if (debug.on()) + debug.log("handling HTTP/3 connection creation result"); + if (t == null && exchange.multi.requestCancelled()) { + return MinimalFuture.failedFuture(new IOException("Request cancelled")); + } + if (c == null && t == null) { + if (debug.on()) + debug.log("downgrading to HTTP/2"); + return attemptHttp2Exchange(exchange, connection); + } else if (t != null) { + t = Utils.getCompletionCause(t); + if (debug.on()) { + if (t instanceof HttpConnectTimeoutException || t instanceof ConnectException) { + debug.log("HTTP/3 connection creation failed: " + t); + } else { + debug.log("HTTP/3 connection creation failed " + + "with unexpected exception:", t); + } + } + return MinimalFuture.failedFuture(t); + } else { + if (debug.on()) + debug.log("creating HTTP/3 exchange"); + try { + if (exchange.hasReachedStreamLimit()) { + // clear the flag before attempting to create a stream again + exchange.streamLimitReached(false); + } + return c.createStream(exchange) + .thenApply(ExchangeImpl::checkCancelled); + } catch (IOException e) { + return MinimalFuture.failedFuture(e); + } + } + } + + private static > T checkCancelled(T exchangeImpl) { + Exchange e = exchangeImpl.getExchange(); + if (debug.on()) { + debug.log("checking cancellation for: " + exchangeImpl); + } + if (e.multi.requestCancelled()) { + if (debug.on()) { + debug.log("request was cancelled"); + } + if (!exchangeImpl.isCanceled()) { + if (debug.on()) { + debug.log("cancelling exchange: " + exchangeImpl); + } + var cause = e.getCancelCause(); + if (cause == null) cause = new IOException("Request cancelled"); + exchangeImpl.cancel(cause); + } + } + return exchangeImpl; + } + + + // Creates an HTTP/2 exchange, possibly downgrading to HTTP/1 private static CompletableFuture> createExchangeImpl(Http2Connection c, Throwable t, @@ -280,12 +676,4 @@ abstract class ExchangeImpl { // an Expect-Continue void expectContinueFailed(int rcode) { } - final boolean isUnprocessedByPeer() { - return this.unprocessedByPeer; - } - - // Marks the exchange as unprocessed by the peer - final void markUnprocessedByPeer() { - this.unprocessedByPeer = true; - } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java new file mode 100644 index 00000000000..3eb4d631c75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; + +/** + * Verifies that when a HTTP3 frame arrives on a stream, then that particular frame type + * is in the expected order as compared to the previous frame type that was received. + * In effect, does what the RFC-9114, section 4.1 and section 6.2.1 specifies. + * Note that the H3FrameOrderVerifier is only responsible for checking the order in which a + * frame type is received on a stream. It isn't responsible for checking if that particular frame + * type is expected to be received on a particular stream type. + */ +abstract class H3FrameOrderVerifier { + long currentProcessingFrameType = -1; // -1 implies no frame being processed currently + long lastCompletedFrameType = -1; // -1 implies no frame processing has completed yet + + /** + * {@return a frame order verifier for HTTP3 request/response stream} + */ + static H3FrameOrderVerifier newForRequestResponseStream() { + return new ResponseStreamVerifier(false); + } + + /** + * {@return a frame order verifier for HTTP3 push promise stream} + */ + static H3FrameOrderVerifier newForPushPromiseStream() { + return new ResponseStreamVerifier(true); + } + + /** + * {@return a frame order verifier for HTTP3 control stream} + */ + static H3FrameOrderVerifier newForControlStream() { + return new ControlStreamVerifier(); + } + + /** + * @param frame The frame that has been received + * {@return true if the {@code frameType} processing can start. false otherwise} + */ + abstract boolean allowsProcessing(final Http3Frame frame); + + /** + * Marks the receipt of complete content of a frame that was currently being processed + * + * @param frame The frame whose content was fully received + * @throws IllegalStateException If the passed frame type wasn't being currently processed + */ + void completed(final Http3Frame frame) { + if (frame instanceof UnknownFrame) { + return; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != frameType) { + throw new IllegalStateException("Unexpected completion of processing " + + "of frame type (" + frameType + "): " + + Http3FrameType.asString(frameType) + ", expected " + + Http3FrameType.asString(currentProcessingFrameType)); + } + currentProcessingFrameType = -1; + lastCompletedFrameType = frameType; + } + + private static final class ControlStreamVerifier extends H3FrameOrderVerifier { + + @Override + boolean allowsProcessing(final Http3Frame frame) { + if (frame instanceof MalformedFrame) { + // a malformed frame can come in any time, so we allow it to be processed + // and we don't "track" it either + return true; + } + if (frame instanceof UnknownFrame) { + // unknown frames can come in any time, we allow them to be processed + // and we don't track their processing/completion. However, if an unknown frame + // is the first frame on a control stream then that's an error and we return "false" + // to prevent processing that frame. + // RFC-9114, section 9, which states - "where a known frame type is required to be + // in a specific location, such as the SETTINGS frame as the first frame of the + // control stream, an unknown frame type does not satisfy that requirement and + // SHOULD be treated as an error" + return lastCompletedFrameType != -1; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != -1) { + // we are in the middle of processing a particular frame type and we + // only expect additional frames of only that type + return frameType == currentProcessingFrameType; + } + // we are not currently processing any frame + if (lastCompletedFrameType == -1) { + // there was no previous frame either, so this is the first frame to have been + // received + if (frameType != SettingsFrame.TYPE) { + // unexpected first frame type + return false; + } + currentProcessingFrameType = frameType; + // expected first frame type + return true; + } + // there's no specific ordering specified on control stream other than expecting + // the SETTINGS frame to be the first received (which we have already verified before + // reaching here) + currentProcessingFrameType = frameType; + return true; + } + } + + private static final class ResponseStreamVerifier extends H3FrameOrderVerifier { + private boolean headerSeen; + private boolean dataSeen; + private boolean trailerCompleted; + private final boolean pushStream; + + private ResponseStreamVerifier(boolean pushStream) { + this.pushStream = pushStream; + } + + @Override + boolean allowsProcessing(final Http3Frame frame) { + if (frame instanceof MalformedFrame) { + // a malformed frame can come in any time, so we allow it to be processed + // and we don't track their processing/completion + return true; + } + if (frame instanceof UnknownFrame) { + // unknown frames can come in any time, we allow them to be processed + // and we don't track their processing/completion + return true; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != -1) { + // we are in the middle of processing a particular frame type and we + // only expect additional frames of only that type + return frameType == currentProcessingFrameType; + } + if (frameType == DataFrame.TYPE) { + if (!headerSeen || trailerCompleted) { + // DATA is not permitted before HEADERS or after trailer + return false; + } + dataSeen = true; + } else if (frameType == HeadersFrame.TYPE) { + if (trailerCompleted) { + // HEADERS is not permitted after trailer + return false; + } + headerSeen = true; + if (dataSeen) { + trailerCompleted = true; + } + } else if (frameType == PushPromiseFrame.TYPE) { + // a push promise is only permitted on a response, + // and not on a push stream + if (pushStream) { + return false; + } + } else { + // no other frames permitted + return false; + } + + currentProcessingFrameType = frameType; + return true; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java index ecc4a63c9d0..02ce63b6314 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java @@ -244,7 +244,7 @@ class Http1Exchange extends ExchangeImpl { this.connection = connection; } else { InetSocketAddress addr = request.getAddress(); - this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1); + this.connection = HttpConnection.getConnection(addr, client, exchange, request, HTTP_1_1); } this.requestAction = new Http1Request(request, this); this.asyncReceiver = new Http1AsyncReceiver(executor, this); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java index 815b6bad20c..8d28b664036 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java @@ -290,7 +290,8 @@ class Http1Request { } String uriString = requestURI(); StringBuilder sb = new StringBuilder(64); - sb.append(request.method()) + String method = request.method(); + sb.append(method) .append(' ') .append(uriString) .append(" HTTP/1.1\r\n"); @@ -300,11 +301,15 @@ class Http1Request { systemHeadersBuilder.setHeader("Host", hostString()); } - // GET, HEAD and DELETE with no request body should not set the Content-Length header if (requestPublisher != null) { contentLength = requestPublisher.contentLength(); if (contentLength == 0) { - systemHeadersBuilder.setHeader("Content-Length", "0"); + // PUT and POST with no request body should set the Content-Length header + // even when the content is empty. + // Other methods defined in RFC 9110 should not send the header in that case. + if ("POST".equals(method) || "PUT".equals(method)) { + systemHeadersBuilder.setHeader("Content-Length", "0"); + } } else if (contentLength > 0) { systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength)); streaming = false; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java index 92a48d901ff..cc8a2a7142b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java @@ -76,7 +76,7 @@ class Http2ClientImpl { /** * When HTTP/2 requested only. The following describes the aggregate behavior including the - * calling code. In all cases, the HTTP2 connection cache + * calling code. In all cases, the HTTP/2 connection cache * is checked first for a suitable connection and that is returned if available. * If not, a new connection is opened, except in https case when a previous negotiate failed. * In that case, we want to continue using http/1.1. When a connection is to be opened and @@ -144,6 +144,7 @@ class Http2ClientImpl { if (conn != null) { try { conn.reserveStream(true, exchange.pushEnabled()); + exchange.connectionAborter.clear(conn.connection); } catch (IOException e) { throw new UncheckedIOException(e); // shouldn't happen } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java index c33cc93e7dd..63889fa6af2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java @@ -33,6 +33,7 @@ import java.lang.invoke.VarHandle; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -70,6 +71,7 @@ import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.common.ValidatingHeadersConsumer; import jdk.internal.net.http.common.ValidatingHeadersConsumer.Context; +import jdk.internal.net.http.frame.AltSvcFrame; import jdk.internal.net.http.frame.ContinuationFrame; import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.ErrorFrame; @@ -90,6 +92,7 @@ import jdk.internal.net.http.hpack.Decoder; import jdk.internal.net.http.hpack.DecodingCallback; import jdk.internal.net.http.hpack.Encoder; import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame; import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH; import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE; import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_CONNECTION_WINDOW_SIZE; @@ -527,6 +530,7 @@ class Http2Connection { AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection) HttpConnection.getConnection(request.getAddress(), h2client.client(), + exchange, request, HttpClient.Version.HTTP_2); @@ -635,6 +639,32 @@ class Http2Connection { return true; } + void abandonStream() { + boolean shouldClose = false; + stateLock.lock(); + try { + long reserved = --numReservedClientStreams; + assert reserved >= 0; + if (finalStream && reserved == 0 && streams.isEmpty()) { + shouldClose = true; + } + } catch (Throwable t) { + shutdown(t); // in case the assert fires... + } finally { + stateLock.unlock(); + } + + // We should close the connection here if + // it's not pooled. If it's not pooled it will + // be marked final stream, reserved will be 0 + // after decrementing it by one, and there should + // be no active request-response streams. + if (shouldClose) { + shutdown(new IOException("HTTP/2 connection abandoned")); + } + + } + boolean shouldClose() { stateLock.lock(); try { @@ -1218,6 +1248,8 @@ class Http2Connection { case PingFrame.TYPE -> handlePing((PingFrame) frame); case GoAwayFrame.TYPE -> handleGoAway((GoAwayFrame) frame); case WindowUpdateFrame.TYPE -> handleWindowUpdate((WindowUpdateFrame) frame); + case AltSvcFrame.TYPE -> processAltSvcFrame(0, (AltSvcFrame) frame, + connection, connection.client()); default -> protocolError(ErrorFrame.PROTOCOL_ERROR); } @@ -1323,7 +1355,8 @@ class Http2Connection { try { // idleConnectionTimeoutEvent is always accessed within a lock protected block if (streams.isEmpty() && idleConnectionTimeoutEvent == null) { - idleConnectionTimeoutEvent = client().idleConnectionTimeout() + final HttpClient.Version version = Version.HTTP_2; + idleConnectionTimeoutEvent = client().idleConnectionTimeout(version) .map(IdleConnectionTimeoutEvent::new) .orElse(null); if (idleConnectionTimeoutEvent != null) { @@ -1367,6 +1400,7 @@ class Http2Connection { String protocolError = "protocol error" + (msg == null?"":(": " + msg)); ProtocolException protocolException = new ProtocolException(protocolError); + this.cause.compareAndSet(null, protocolException); if (markHalfClosedLocal()) { framesDecoder.close(protocolError); subscriber.stop(protocolException); @@ -1844,8 +1878,16 @@ class Http2Connection { } finally { Throwable x = errorRef.get(); if (x != null) { - if (debug.on()) debug.log("Stopping scheduler", x); scheduler.stop(); + if (client2.stopping()) { + if (debug.on()) { + debug.log("Stopping scheduler"); + } + } else { + if (debug.on()) { + debug.log("Stopping scheduler", x); + } + } Http2Connection.this.shutdown(x); } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java new file mode 100644 index 00000000000..05b27c3d529 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java @@ -0,0 +1,844 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.UnsupportedProtocolVersionException; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.ConnectionExpiredException; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.quic.QuicTLSContext; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.internal.net.http.Http3ClientProperties.WAIT_FOR_PENDING_CONNECT; +import static jdk.internal.net.http.common.Alpns.H3; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; + +/** + * Http3 specific aspects of HttpClientImpl + */ +final class Http3ClientImpl implements AutoCloseable { + // Setting this property disables HTTPS hostname verification. Use with care. + private static final boolean disableHostnameVerification = Utils.isHostnameVerificationDisabled(); + // QUIC versions in their descending order of preference + private static final List availableQuicVersions; + static { + // we default to QUIC v1 followed by QUIC v2, if no specific preference cannot be + // determined + final List defaultPref = List.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2); + // check user specified preference + final String sysPropVal = Utils.getProperty("jdk.httpclient.quic.available.versions"); + if (sysPropVal == null || sysPropVal.isBlank()) { + // default to supporting both v1 and v2, with v1 given preference + availableQuicVersions = defaultPref; + } else { + final List descendingPref = new ArrayList<>(); + for (final String val : sysPropVal.split(",")) { + final QuicVersion qv; + try { + // parse QUIC version number represented as a hex string + final var vernum = Integer.parseInt(val.trim(), 16); + qv = QuicVersion.of(vernum).orElse(null); + } catch (NumberFormatException nfe) { + // ignore and continue with next + continue; + } + if (qv == null) { + continue; + } + descendingPref.add(qv); + } + availableQuicVersions = descendingPref.isEmpty() ? defaultPref : descendingPref; + } + } + + private final Logger debug = Utils.getDebugLogger(this::dbgString); + + final HttpClientImpl client; + private final Http3ConnectionPool connections = new Http3ConnectionPool(debug); + private final Http3PendingConnections reconnections = new Http3PendingConnections(); + private final Set pendingClose = ConcurrentHashMap.newKeySet(); + private final Set noH3 = ConcurrentHashMap.newKeySet(); + + private final QuicClient quicClient; + private volatile boolean closed; + private final AtomicReference errorRef = new AtomicReference<>(); + private final ReentrantLock lock = new ReentrantLock(); + + Http3ClientImpl(HttpClientImpl client) { + this.client = client; + var executor = client.theExecutor().safeDelegate(); + var context = client.theSSLContext(); + var parameters = client.sslParameters(); + if (!disableHostnameVerification) { + // setting the endpoint identification algo to HTTPS ensures that + // during the TLS handshake, the cert presented by the server is verified + // for hostname checks against the SNI hostname(s) set by the client + // or in its absence the peer's hostname. + // see sun.security.ssl.X509TrustManagerImpl#checkIdentity(...) + parameters.setEndpointIdentificationAlgorithm("HTTPS"); + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicClient.Builder builder = new QuicClient.Builder(); + builder.availableVersions(availableQuicVersions) + .tlsContext(quicTLSContext) + .sslParameters(parameters) + .executor(executor) + .applicationErrors(Http3Error::stringForCode) + .clientId(client.dbgString()); + if (client.localAddress() != null) { + builder.bindAddress(new InetSocketAddress(client.localAddress(), 0)); + } + final QuicTransportParameters transportParameters = new QuicTransportParameters(); + // HTTP/3 doesn't allow remote bidirectional stream + transportParameters.setIntParameter(initial_max_streams_bidi, 0); + // HTTP/3 doesn't allow remote bidirectional stream: no need to allow data + transportParameters.setIntParameter(initial_max_stream_data_bidi_remote, 0); + builder.transportParameters(transportParameters); + this.quicClient = builder.build(); + } + + // Records an exchange waiting for a connection recovery to complete. + // A connection recovery happens when a connection has maxed out its number + // of streams, and no MAX_STREAM frame has arrived. In that case, the connection + // is abandoned (marked with setFinalStream() and taken out of the pool) and a + // new connection is initiated. Waiters are waiting for the new connection + // handshake to finish and for the connection to be put in the pool. + record Waiter(MinimalFuture cf, HttpRequestImpl request, Exchange exchange) { + void complete(Http3Connection conn, Throwable error) { + if (error != null) cf.completeExceptionally(error); + else cf.complete(conn); + } + static Waiter of(HttpRequestImpl request, Exchange exchange) { + return new Waiter(new MinimalFuture<>(), request, exchange); + } + } + + // Indicates that recovery is needed, or in progress, for a given + // connection + sealed interface ConnectionRecovery permits PendingConnection, StreamLimitReached { + } + + // Indicates that recovery of a connection has been initiated. + // Waiters will be put in wait until the handshake is completed + // and the connection is inserted in the pool + record PendingConnection(AltService altSvc, Exchange exchange, ConcurrentLinkedQueue waiters) + implements ConnectionRecovery { + PendingConnection(AltService altSvc, Exchange exchange, ConcurrentLinkedQueue waiters) { + this.altSvc = altSvc; + this.waiters = Objects.requireNonNull(waiters); + this.exchange = exchange; + } + PendingConnection(AltService altSvc, Exchange exchange) { + this(altSvc, exchange, new ConcurrentLinkedQueue<>()); + } + } + + // Indicates that a connection that was in the pool has maxed out + // its stream limit and will be taken out of the pool. A new connection + // will be created for the first request/response exchange that needs + // it. + record StreamLimitReached(Http3Connection connection) implements ConnectionRecovery {} + + // Called when recovery is needed for a given connection, with + // the request that got the StreamLimitException + public void streamLimitReached(Http3Connection connection, HttpRequestImpl request) { + lock.lock(); + try { + reconnections.streamLimitReached(connectionKey(request), connection); + } finally { + lock.unlock(); + } + } + + HttpClientImpl client() { + return client; + } + + String dbgString() { + return "Http3ClientImpl(" + client.dbgString() + ")"; + } + + QuicClient quicClient() { + return this.quicClient; + } + + String connectionKey(HttpRequestImpl request) { + return connections.connectionKey(request); + } + + Http3Connection findPooledConnectionFor(HttpRequestImpl request, + Exchange exchange) + throws IOException { + if (request.secure() && request.proxy() == null) { + final var pooled = connections.lookupFor(request); + if (pooled == null) { + return null; + } + if (pooled.tryReserveForPoolCheckout() && !pooled.isFinalStream()) { + final var altService = pooled.connection() + .getSourceAltService().orElse(null); + if (altService != null) { + // if this connection was created because it was advertised by some alt-service + // then verify that the alt-service is still valid/active + if (altService.wasAdvertised() && !client.registry().isActive(altService)) { + if (debug.on()) { + debug.log("Alt-Service %s for pooled connection has expired," + + " marking the connection as unusable for new streams", altService); + } + // alt-service that was the reason for this H3 connection to be created (and pooled) + // is no longer valid. We set a state on the connection to disallow any new streams + // and be auto-closed when all current streams are done + pooled.setFinalStreamAndCloseIfIdle(); + return null; + } + } + if (debug.on()) { + debug.log("Found Http3Connection in connection pool"); + } + // found a valid connection in pool, return it + return pooled; + } else { + if (debug.on()) { + debug.log("Pooled connection expired. Removing it."); + } + removeFromPool(pooled); + } + } + return null; + } + + private static String label(Http3Connection conn) { + return Optional.ofNullable(conn) + .map(Http3Connection::connection) + .map(HttpQuicConnection::label) + .orElse("null"); + } + + private static String describe(HttpRequestImpl request, long id) { + return String.format("%s #%s", request, id); + } + + private static String describe(Exchange exchange) { + if (exchange == null) return "null"; + return describe(exchange.request, exchange.multi.id); + } + + private static String describePendingExchange(String prefix, PendingConnection pending) { + return String.format("%s %s", prefix, describe(pending.exchange)); + } + + private static String describeAltSvc(PendingConnection pendingConnection) { + return Optional.ofNullable(pendingConnection) + .map(PendingConnection::altSvc) + .map(AltService::toString) + .map(s -> "altsvc: " + s) + .orElse("no altSvc"); + } + + // Called after a recovered connection has been put back in the pool + // (or when recovery has failed), or when a new connection handshake + // has completed. + // Waiters, if any, will be notified. + private void connectionCompleted(String connectionKey, Exchange origExchange, Http3Connection conn, Throwable error) { + try { + if (Log.http3()) { + Log.logHttp3("Checking waiters on completed connection {0} to {1} created for {2}", + label(conn), connectionKey, describe(origExchange)); + } + connectionCompleted0(connectionKey, origExchange, conn, error); + } catch (Throwable t) { + if (Log.http3() || Log.errors()) { + Log.logError(t); + } + throw t; + } + } + + private void connectionCompleted0(String connectionKey, Exchange origExchange, Http3Connection conn, Throwable error) { + lock.lock(); + // There should be a connection in the pool at this point, + // so we can remove the PendingConnection from the reconnections list; + PendingConnection pendingConnection = null; + try { + var recovery = reconnections.removeCompleted(connectionKey, origExchange, conn); + if (recovery instanceof PendingConnection pending) { + pendingConnection = pending; + } + } finally { + lock.unlock(); + } + if (pendingConnection == null) { + if (Log.http3()) { + Log.logHttp3("No waiters to complete for " + label(conn)); + } + return; + } + + int waitersCount = pendingConnection.waiters.size(); + if (waitersCount != 0 && Log.http3()) { + Log.logHttp3("Completing " + waitersCount + + " waiters on recreated connection " + label(conn) + + describePendingExchange(" - originally created for", pendingConnection)); + } + + // now for each waiter we're going to try to complete it. + // however, there may be more waiters than available streams! + // so it's rinse and repeat at this point + boolean origExchangeCancelled = origExchange == null ? false : origExchange.multi.requestCancelled(); + int completedWaiters = 0; + int errorWaiters = 0; + int retriedWaiters = 0; + try { + while (!pendingConnection.waiters.isEmpty()) { + var waiter = pendingConnection.waiters.poll(); + if (error != null && (!origExchangeCancelled || waiter.exchange == origExchange)) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + error); + } else if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with " + conn + " error=" + error); + } + errorWaiters++; + waiter.complete(conn, error); + } else { + var request = waiter.request; + var exchange = waiter.exchange; + try { + Http3Connection pooled = findPooledConnectionFor(request, exchange); + if (pooled != null && !pooled.isFinalStream() && !waiter.cf.isDone()) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + label(pooled)); + } else if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with pooled conn " + label(pooled)); + } + completedWaiters++; + waiter.cf.complete(pooled); + } else if (!waiter.cf.isDone()) { + // we call getConnectionFor: it should put waiter in the + // new waiting list, or attempt to open a connection again + if (conn != null) { + if (Log.http3()) { + Log.logHttp3("Not enough streams on recreated connection for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + label(conn)); + } else if (debug.on()) { + debug.log("Not enough streams on recreated connection for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with " + label(conn) + + ": retrying on new connection"); + } + retriedWaiters++; + getConnectionFor(request, exchange, waiter); + } else { + if (Log.http3()) { + Log.logHttp3("No HTTP/3 connection for:: " + waiter.request + " #" + + waiter.exchange.multi.id + ": will downgrade or fail"); + } else if (debug.on()) { + debug.log("No HTTP/3 connection for: " + waiter.request + + " #" + waiter.exchange.multi.id + ": will downgrade or fail"); + } + completedWaiters++; + waiter.complete(null, error); + } + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with error: " + + Utils.getCompletionCause(t)); + } + var cause = Utils.getCompletionCause(t); + if (cause instanceof ClosedChannelException) { + cause = new ConnectionExpiredException(cause); + } + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + cause); + } + errorWaiters++; + waiter.cf.completeExceptionally(cause); + } + } + } + } finally { + if (Log.http3()) { + String pendingInfo = describePendingExchange(" - originally created for", pendingConnection); + + if (conn != null) { + Log.logHttp3(("Connection creation completed for requests to %s: " + + "waiters[%s](completed:%s, retried:%s, errors:%s)%s") + .formatted(connectionKey, waitersCount, completedWaiters, + retriedWaiters, errorWaiters, pendingInfo)); + } else { + Log.logHttp3(("No HTTP/3 connection created for requests to %s, will fail or downgrade: " + + "waiters[%s](completed:%s, retried:%s, errors:%s)%s") + .formatted(connectionKey, waitersCount, completedWaiters, + retriedWaiters, errorWaiters, pendingInfo)); + } + } + } + } + + CompletableFuture getConnectionFor(HttpRequestImpl request, Exchange exchange) { + assert request != null; + return getConnectionFor(request, exchange, null); + } + + private void completeWaiter(Logger debug, Waiter pendingWaiter, Http3Connection r, Throwable t) { + // the recovery was done on behalf of a pending waiter. + // this can happen if the new connection has already maxed out, + // and recovery was initiated on behalf of the next waiter. + if (Log.http3()) { + Log.logHttp3("Completing waiter for: " + pendingWaiter.request + " #" + + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")"); + } else if (debug.on()) { + debug.log("Completing pending waiter for " + pendingWaiter.request + " #" + + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")"); + } + pendingWaiter.complete(r, t); + } + + private CompletableFuture wrapForDebug(CompletableFuture h3Cf, + Exchange exchange, + HttpRequestImpl request) { + if (debug.on() || Log.http3()) { + if (Log.http3()) { + Log.logHttp3("Recreating connection for: " + request + " #" + + exchange.multi.id); + } else if (debug.on()) { + debug.log("Recreating connection for: " + request + " #" + + exchange.multi.id); + } + return h3Cf.whenComplete((r, t) -> { + if (Log.http3()) { + if (r != null && t == null) { + Log.logHttp3("Connection recreated for " + request + " #" + + exchange.multi.id + " on " + label(r)); + } else if (t != null) { + Log.logHttp3("Connection creation failed for " + request + " #" + + exchange.multi.id + ": " + t); + } else if (r == null) { + Log.logHttp3("No connection found for " + request + " #" + + exchange.multi.id); + } + } else if (debug.on()) { + debug.log("Connection recreated for " + request + " #" + + exchange.multi.id); + } + }); + } else { + return h3Cf; + } + } + + Optional lookupAltSvc(HttpRequestImpl request) { + return client.registry() + .lookup(request.uri(), H3::equals) + .findFirst(); + } + + CompletableFuture getConnectionFor(HttpRequestImpl request, + Exchange exchange, + Waiter pendingWaiter) { + assert request != null; + if (Log.http3()) { + if (pendingWaiter != null) { + Log.logHttp3("getConnectionFor pendingWaiter {0}", + describe(pendingWaiter.request, pendingWaiter.exchange.multi.id)); + } else { + Log.logHttp3("getConnectionFor exchange {0}", + describe(request, exchange.multi.id)); + } + } + try { + Http3Connection pooled = findPooledConnectionFor(request, exchange); + if (pooled != null) { + if (pendingWaiter != null) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } else if (debug.on()) { + debug.log("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } + pendingWaiter.cf.complete(pooled); + return pendingWaiter.cf; + } else { + return MinimalFuture.completedFuture(pooled); + } + } + if (request.secure() && request.proxy() == null) { + boolean reconnecting, waitForPendingConnect; + PendingConnection pendingConnection = null; + String key; + Waiter waiter = null; + if (reconnecting = exchange.hasReachedStreamLimit()) { + if (debug.on()) { + debug.log("Exchange has reached limit for: " + request + " #" + + exchange.multi.id); + } + } + if (pendingWaiter != null) reconnecting = true; + lock.lock(); + try { + key = connectionKey(request); + + var recovery = reconnections.lookupFor(key, request, client); + if (debug.on()) debug.log("lookup found %s for %s", recovery, request); + if (recovery instanceof PendingConnection pending) { + // Recovery already initiated. Add waiter to the list! + if (debug.on()) { + debug.log("PendingConnection (%s) found for %s", + describePendingExchange("originally created for", pending), + describe(request, exchange.multi.id)); + } + pendingConnection = pending; + waiter = pendingWaiter == null + ? Waiter.of(request, exchange) + : pendingWaiter; + exchange.streamLimitReached(false); + pendingConnection.waiters.add(waiter); + return waiter.cf; + } else if (recovery instanceof StreamLimitReached) { + // A connection to this server has maxed out its allocated + // streams and will be taken out of the pool, but recovery + // has not been initiated yet. Do that now. + reconnecting = waitForPendingConnect = true; + } else waitForPendingConnect = WAIT_FOR_PENDING_CONNECT; + // By default, we allow concurrent attempts to + // create HTTP/3 connections to the same host, except when + // one connection has reached the maximum number of streams + // it is allowed to use. However, + // if waitForPendingConnect is set to `true` above we will + // only allow one connection to attempt handshake at a given + // time, other requests will be added to a pending list so + // that they can go through that connection. + if (waitForPendingConnect) { + // check again + if ((pooled = findPooledConnectionFor(request, exchange)) == null) { + // initiate recovery + var altSvc = lookupAltSvc(request).orElse(null); + // maybe null if ALT_SVC && altSvc == null + pendingConnection = reconnections.addPending(key, request, altSvc, exchange); + } else if (pendingWaiter != null) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } else if (debug.on()) { + debug.log("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } + pendingWaiter.cf.complete(pooled); + return pendingWaiter.cf; + } else { + return MinimalFuture.completedFuture(pooled); + } + } + } finally { + lock.unlock(); + if (waiter != null && waiter != pendingWaiter && Log.http3()) { + var altSvc = describeAltSvc(pendingConnection); + var orig = Optional.of(pendingConnection) + .map(PendingConnection::exchange) + .map(e -> " created for #" + e.multi.id) + .orElse(""); + Log.logHttp3("Waiting for connection for: " + describe(request, exchange.multi.id) + + " " + altSvc + orig); + } else if (pendingWaiter != null && Log.http3()) { + var altSvc = describeAltSvc(pendingConnection); + Log.logHttp3("Creating connection for: " + describe(request, exchange.multi.id) + + " " + altSvc); + } else if (debug.on() && waiter != null) { + debug.log("Waiting for connection for: " + describe(request, exchange.multi.id) + + (waiter == pendingWaiter ? " (still pending)" : "")); + } + } + + if (Log.http3()) { + Log.logHttp3("Creating connection for Exchange {0}", describe(exchange)); + } else if (debug.on()) { + debug.log("Creating connection for Exchange %s", describe(exchange)); + } + + CompletableFuture h3Cf = Http3Connection + .createAsync(request, this, exchange); + if (reconnecting) { + // System.err.println("Recreating connection for: " + request + " #" + // + exchange.multi.id); + h3Cf = wrapForDebug(h3Cf, exchange, request); + } + if (pendingWaiter != null) { + // the connection was done on behalf of a pending waiter. + // this can happen if the new connection has already maxed out, + // and recovery was initiated on behalf of the next waiter. + h3Cf = h3Cf.whenComplete((r,t) -> completeWaiter(debug, pendingWaiter, r, t)); + } + h3Cf = h3Cf.thenApply(conn -> { + if (conn != null) { + if (debug.on()) { + debug.log("Offering connection %s created for %s", + label(conn), exchange.multi.id); + } + var offered = offerConnection(conn); + if (debug.on()) { + debug.log("Connection offered %s created for %s", + label(conn), exchange.multi.id); + } + // if we return null here, we will downgrade + // but if we return `conn` we will open a new connection. + return offered == null ? conn : offered; + } else { + if (debug.on()) { + debug.log("No connection for exchange #" + exchange.multi.id); + } + return null; + } + }); + if (pendingConnection != null) { + // need to wake up waiters after successful handshake and recovery + h3Cf = h3Cf.whenComplete((r, t) -> connectionCompleted(key, exchange, r, t)); + } + return h3Cf; + } else { + if (debug.on()) + debug.log("Request is unsecure, or proxy isn't null: can't use HTTP/3"); + if (request.isHttp3Only(exchange.version())) { + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "can't use HTTP/3 with proxied or unsecured connection")); + } + return MinimalFuture.completedFuture(null); + } + } catch (Throwable t) { + if (Log.http3() || Log.errors()) { + Log.logError("Failed to get connection for {0}: {1}", + describe(exchange), t); + } + return MinimalFuture.failedFuture(t); + } + } + + /* + * Cache the given connection, if no connection to the same + * destination exists. If one exists, then we let the initial stream + * complete but allow it to close itself upon completion. + * This situation should not arise with https because the request + * has not been sent as part of the initial alpn negotiation + */ + Http3Connection offerConnection(Http3Connection c) { + if (debug.on()) debug.log("offering to the connection pool: %s", c); + if (!c.isOpen() || c.isFinalStream()) { + if (debug.on()) + debug.log("skipping offered closed or closing connection: %s", c); + return null; + } + + String key = c.key(); + lock.lock(); + try { + if (closed) { + var error = errorRef.get(); + if (error == null) error = new IOException("client closed"); + c.connectionError(error, Http3Error.H3_INTERNAL_ERROR); + return null; + } + Http3Connection c1 = connections.putIfAbsent(key, c); + if (c1 != null) { + // there was a connection in the pool + if (!c1.isFinalStream() || c.isFinalStream()) { + if (!c.isFinalStream()) { + c.allowOnlyOneStream(); + return c; + } else if (c1.isFinalStream()) { + return c; + } + if (debug.on()) + debug.log("existing entry %s in connection pool for %s", c1, key); + // c1 will remain in the pool and we will use c for the given + // request. + if (Log.http3()) { + Log.logHttp3("Existing connection {0} for {1} found in the pool", label(c1), c1.key()); + Log.logHttp3("New connection {0} marked final and not offered to the pool", label(c)); + } + return c1; + } + connections.put(key, c); + } + if (debug.on()) + debug.log("put in the connection pool: %s", c); + return c; + } finally { + lock.unlock(); + } + } + + void removeFromPool(Http3Connection c) { + lock.lock(); + try { + if (connections.remove(c.key(), c)) { + if (debug.on()) + debug.log("removed from the connection pool: %s", c); + } + if (c.isOpen()) { + if (debug.on()) + debug.log("adding to pending close: %s", c); + pendingClose.add(c); + } + } finally { + lock.unlock(); + } + } + + void connectionClosed(Http3Connection c) { + removeFromPool(c); + if (pendingClose.remove(c)) { + if (debug.on()) + debug.log("removed from pending close: %s", c); + } + } + + public Logger debug() { return debug;} + + @Override + public void close() { + try { + lock.lock(); + try { + closed = true; + pendingClose.clear(); + connections.clear(); + } finally { + lock.unlock(); + } + // The client itself is being closed, so we don't individually close the connections + // here and instead just close the QuicClient which then initiates the close of + // the QUIC endpoint. That will silently terminate the underlying QUIC connections + // without exchanging any datagram packets with the peer, since there's no point + // sending/receiving those (including GOAWAY frame) when the endpoint (socket channel) + // itself won't be around after this point. + } finally { + quicClient.close(); + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + if (debug.on()) { + debug.log("HTTP/3 client aborting due to " + t); + } + try { + errorRef.compareAndSet(null, t); + List connectionList; + lock.lock(); + try { + closed = true; + connectionList = new ArrayList<>(connections.values().toList()); + connectionList.addAll(pendingClose); + pendingClose.clear(); + connections.clear(); + } finally { + lock.unlock(); + } + for (var conn : connectionList) { + conn.close(t); + } + } finally { + quicClient.abort(t); + } + } + + public void stop() { + close(); + } + + /** + * After an unsuccessful H3 direct connection attempt, + * mark the authority as not supporting h3. + * @param rawAuthority the raw authority (host:port) + */ + public void noH3(String rawAuthority) { + noH3.add(rawAuthority); + } + + /** + * Tells whether the given authority has been marked as + * not supporting h3 + * @param rawAuthority the raw authority (host:port) + * @return true if the given authority is believed to not support h3 + */ + public boolean hasNoH3(String rawAuthority) { + return noH3.contains(rawAuthority); + } + + /** + * A direct HTTP/3 attempt may be attempted if we don't have an + * AltService h3 endpoint recorded for it, and if the given request + * URI's raw authority hasn't been marked as not supporting HTTP/3, + * and if the request discovery config is not ALT_SVC. + * Note that a URI may be marked has not supporting H3 if it doesn't + * acknowledge the first initial quic packet in the time defined + * by {@systemProperty jdk.httpclient.http3.maxDirectConnectionTimeout}. + * @param request the request that may go through h3 + * @return true if there's no h3 endpoint already registered for the given uri. + */ + public boolean mayAttemptDirectConnection(HttpRequestImpl request) { + var config = request.http3Discovery(); + return switch (config) { + // never attempt direct connection with ALT_SVC + case Http3DiscoveryMode.ALT_SVC -> false; + // always attempt direct connection with HTTP_3_ONLY, unless + // it was attempted before and failed + case Http3DiscoveryMode.HTTP_3_URI_ONLY -> + !hasNoH3(request.uri().getRawAuthority()); + // otherwise, attempt direct connection only if we have no + // alt service and it wasn't attempted and failed before + default -> lookupAltSvc(request).isEmpty() + && !hasNoH3(request.uri().getRawAuthority()); + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java new file mode 100644 index 00000000000..81f8c8109d5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import jdk.internal.net.http.common.Utils; + +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY; + +/** + * A class that groups initial values for HTTP/3 client properties. + *

    + * Properties starting with {@code jdk.internal.} are not exposed and + * typically reserved for testing. They could be removed, and their name, + * semantics, or values, could be changed at any time. + *

    + * Properties that are exposed are JDK specifics and typically documented + * in the {@link java.net.http} module API documentation. + *

      + *
    1. + *
    2. + *
    + * + * @apiNote + * Not all properties are exposed. Properties that are not included in + * the {@link java.net.http} module API documentation are subject to + * change, and should be considered internal, though we might also consider + * exposing them in the future if needed. + * + */ +public final class Http3ClientProperties { + + private Http3ClientProperties() { + throw new InternalError("should not come here"); + } + + // The maximum timeout to wait for a reply to the first INITIAL + // packet when attempting a direct connection + public static final long MAX_DIRECT_CONNECTION_TIMEOUT; + + // The maximum timeout to wait for a MAX_STREAM frame + // before throwing StreamLimitException + public static final long MAX_STREAM_LIMIT_WAIT_TIMEOUT; + + // The maximum number of concurrent push streams + // by connection + public static final long MAX_HTTP3_PUSH_STREAMS; + + // Limit for dynamic table capacity that the encoder is allowed + // to set. Its capacity is also limited by the QPACK_MAX_TABLE_CAPACITY + // HTTP/3 setting value received from the peer decoder. + public static final long QPACK_ENCODER_TABLE_CAPACITY_LIMIT; + + // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long QPACK_DECODER_MAX_TABLE_CAPACITY; + + // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long QPACK_DECODER_MAX_FIELD_SECTION_SIZE; + + // Decoder upper bound on the number of streams that can be blocked + public static final long QPACK_DECODER_BLOCKED_STREAMS; + + // of available space in the dynamic table + + // Percentage of occupied space in the dynamic table that controls when + // the draining index starts increasing. This index determines which entries + // are too close to eviction, and can be referenced by the encoder. + public static final int QPACK_ENCODER_DRAINING_THRESHOLD; + + // If set to "true" allows the encoder to insert a header with a dynamic + // name reference and reference it in a field line section without awaiting + // decoder's acknowledgement. + public static final boolean QPACK_ALLOW_BLOCKING_ENCODING = Utils.getBooleanProperty( + "jdk.internal.httpclient.qpack.allowBlockingEncoding", false); + + // whether localhost is acceptable as an alternative service origin + public static final boolean ALTSVC_ALLOW_LOCAL_HOST_ORIGIN = Utils.getBooleanProperty( + "jdk.httpclient.altsvc.allowLocalHostOrigin", true); + + // whether concurrent HTTP/3 requests to the same host should wait for + // first connection to succeed (or fail) instead of attempting concurrent + // connections. Where concurrent connections are attempted, only one of + // them will be offered to the connection pool. The others will serve a + // single request. + public static final boolean WAIT_FOR_PENDING_CONNECT = Utils.getBooleanProperty( + "jdk.httpclient.http3.waitForPendingConnect", true); + + + static { + // 375 is ~ to the initial loss timer + // 1000 is ~ the initial PTO + // We will set a timeout of 2*1375 ms to wait for the reply to our + // first initial packet for a direct connection + long defaultMaxDirectConnectionTimeout = 1375 << 1; // ms + long maxDirectConnectionTimeout = Utils.getLongProperty( + "jdk.httpclient.http3.maxDirectConnectionTimeout", + defaultMaxDirectConnectionTimeout); + long maxStreamLimitTimeout = Utils.getLongProperty( + "jdk.httpclient.http3.maxStreamLimitTimeout", + defaultMaxDirectConnectionTimeout); + int defaultMaxHttp3PushStreams = Utils.getIntegerProperty( + "jdk.httpclient.maxstreams", + 100); + int maxHttp3PushStreams = Utils.getIntegerProperty( + "jdk.httpclient.http3.maxConcurrentPushStreams", + defaultMaxHttp3PushStreams); + long defaultDecoderMaxCapacity = 0; + long decoderMaxTableCapacity = Utils.getLongProperty( + "jdk.httpclient.qpack.decoderMaxTableCapacity", + defaultDecoderMaxCapacity); + long decoderBlockedStreams = Utils.getLongProperty( + "jdk.httpclient.qpack.decoderBlockedStreams", + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS); + long defaultEncoderTableCapacityLimit = 4096; + long encoderTableCapacityLimit = Utils.getLongProperty( + "jdk.httpclient.qpack.encoderTableCapacityLimit", + defaultEncoderTableCapacityLimit); + int defaultDecoderMaxFieldSectionSize = 393216; // 384kB + long decoderMaxFieldSectionSize = Utils.getIntegerNetProperty( + "jdk.http.maxHeaderSize", Integer.MIN_VALUE, Integer.MAX_VALUE, + defaultDecoderMaxFieldSectionSize, true); + // Percentage of occupied space in the dynamic table that when + // exceeded the dynamic table draining index starts increasing + int drainingThreshold = Utils.getIntegerProperty( + "jdk.internal.httpclient.qpack.encoderDrainingThreshold", + 75); + + MAX_DIRECT_CONNECTION_TIMEOUT = maxDirectConnectionTimeout <= 0 + ? defaultMaxDirectConnectionTimeout : maxDirectConnectionTimeout; + MAX_STREAM_LIMIT_WAIT_TIMEOUT = maxStreamLimitTimeout < 0 + ? defaultMaxDirectConnectionTimeout + : maxStreamLimitTimeout; + MAX_HTTP3_PUSH_STREAMS = Math.max(maxHttp3PushStreams, 0); + QPACK_ENCODER_TABLE_CAPACITY_LIMIT = encoderTableCapacityLimit < 0 + ? defaultEncoderTableCapacityLimit : encoderTableCapacityLimit; + QPACK_DECODER_MAX_TABLE_CAPACITY = decoderMaxTableCapacity < 0 ? + DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY : decoderMaxTableCapacity; + QPACK_DECODER_MAX_FIELD_SECTION_SIZE = decoderMaxFieldSectionSize < 0 ? + DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE : decoderMaxFieldSectionSize; + QPACK_DECODER_BLOCKED_STREAMS = decoderBlockedStreams < 0 ? + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS : decoderBlockedStreams; + QPACK_ENCODER_DRAINING_THRESHOLD = Math.clamp(drainingThreshold, 10, 90); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java new file mode 100644 index 00000000000..b97a441881d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java @@ -0,0 +1,1657 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.net.http.StreamLimitException; +import java.net.http.UnsupportedProtocolVersionException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import jdk.internal.net.http.Http3PushManager.CancelPushReason; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.CancelPushFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.GoAwayFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.MaxPushIdFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicStreamLimitException; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.http.quic.streams.QuicStreams; +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.internal.net.http.Http3ClientProperties.MAX_STREAM_LIMIT_WAIT_TIMEOUT; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; + +/** + * An HTTP/3 connection wraps an HttpQuicConnection and implements + * HTTP/3 on top it. + */ +public final class Http3Connection implements AutoCloseable { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3ClientImpl client; + private final HttpQuicConnection connection; + private final QuicConnection quicConnection; + // key by which this connection will be referred to within the connection pool + private final String connectionKey; + private final String dbgTag; + private final UniStreamPair controlStreamPair; + private final UniStreamPair qpackEncoderStreams; + private final UniStreamPair qpackDecoderStreams; + private final Encoder qpackEncoder; + private final Decoder qpackDecoder; + private final FramesDecoder controlFramesDecoder; + private final Predicate remoteStreamListener; + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForControlStream(); + // streams for HTTP3 exchanges + private final ConcurrentMap exchangeStreams = new ConcurrentHashMap<>(); + private final ConcurrentMap> exchanges = new ConcurrentHashMap<>(); + // true when the settings frame has been received on the control stream of this connection + private volatile boolean settingsFrameReceived; + // the settings we received from the peer + private volatile ConnectionSettings peerSettings; + // the settings we send to our peer + private volatile ConnectionSettings ourSettings; + // for tests + private final MinimalFuture peerSettingsCF = new MinimalFuture<>(); + // the (lowest) request stream id received in GOAWAY frames on this connection. + // subsequent request stream id(s) (if any) must always be equal to lesser than this value + // as per spec + // -1 is used to imply no GOAWAY received so far + private final AtomicLong lowestGoAwayReceipt = new AtomicLong(-1); + private volatile IdleConnectionTimeoutEvent idleConnectionTimeoutEvent; + // value of true implies no more streams will be initiated on this connection, + // and the connection will be closed once the in-progress streams complete. + private volatile boolean finalStream; + private volatile boolean allowOnlyOneStream; + // set to true if we decide to open a new connection + // due to stream limit reached + private volatile boolean streamLimitReached; + + private static final int GOAWAY_SENT = 1; // local endpoint sent GOAWAY + private static final int GOAWAY_RECEIVED = 2; // received GOAWAY from remote peer + private static final int CLOSED = 4; // close called on QUIC connection + volatile int closedState; + + private final ReentrantLock lock = new ReentrantLock(); + private final Http3PushManager pushManager; + private final AtomicLong reservedStreamCount = new AtomicLong(); + + // The largest pushId for a remote created stream. + // After GOAWAY has been sent, we will not accept + // any larger pushId. + private final AtomicLong largestPushId = new AtomicLong(); + + // The max pushId for which a frame was scheduled to be sent. + // This should always be less or equal to pushManager.maxPushId + private final AtomicLong maxPushIdSent = new AtomicLong(); + + + /** + * Creates a new HTTP/3 connection over a given {@link HttpQuicConnection}. + * + * @apiNote + * This constructor is invoked upon a successful quic connection establishment, + * typically after a successful Quic handshake. Creating the Http3Connection + * earlier, for instance, after receiving the Server Hello, could also be considered. + * + * @implNote + * Creating an HTTP/3 connection will trigger the creation of the HTTP/3 control + * stream, sending of the HTTP/3 Settings frame, and creation of the QPack + * encoder/decoder streams. + * + * @param request the request which triggered the creation of the connection + * @param client the Http3Client instance this connection belongs to + * @param connection the {@code HttpQuicConnection} that was established + */ + Http3Connection(HttpRequestImpl request, Http3ClientImpl client, HttpQuicConnection connection) { + this.connectionKey = client.connectionKey(request); + this.client = client; + this.connection = connection; + this.quicConnection = connection.quicConnection(); + var qdb = quicConnection.dbgTag(); + this.dbgTag = "H3(" + qdb +")"; + this.pushManager = new Http3PushManager(this); // OK to leak this + controlFramesDecoder = new FramesDecoder("H3-control("+qdb+")", + FramesDecoder::isAllowedOnControlStream); + controlStreamPair = new UniStreamPair( + StreamType.CONTROL, + quicConnection, + this::processPeerControlBytes, + this::lcsWriterLoop, + this::controlStreamFailed, + debug); + + qpackEncoder = new Encoder(Http3Connection::shouldUpdateDynamicTable, + this::createEncoderStreams, this::connectionError); + qpackEncoderStreams = qpackEncoder.encoderStreams(); + qpackDecoder = new Decoder(this::createDecoderStreams, this::connectionError); + qpackDecoderStreams = qpackDecoder.decoderStreams(); + // Register listener to be called when the peer opens a new stream + remoteStreamListener = this::onOpenRemoteStream; + quicConnection.addRemoteStreamListener(remoteStreamListener); + + // Registers dependent actions with the controlStreamPair + // .futureSenderStreamWriter() CF, in order to send + // the SETTINGS and MAX_PUSHID frames. + // These actions will be executed when the stream writer is + // available. + // + // This will schedule the SETTINGS and MAX_PUSHID frames + // for writing, buffering them if necessary until control + // flow credits are available. + // + // If an exception happens the connection will be + // closed abruptly (by closing the underlying quic connection) + // with an error of type Http3Error.H3_INTERNAL_ERROR + controlStreamPair.futureSenderStreamWriter() + // Send SETTINGS first + .thenApply(this::sendSettings) + // Chains to sending MAX_PUSHID after SETTINGS + .thenApply(this::sendMaxPushId) + // arranges for the connection to be closed + // in case of exception. Throws in the dependent + // action after wrapping the exception if needed. + .exceptionally(this::exceptionallyAndClose); + if (Log.http3()) { + Log.logHttp3("HTTP/3 connection created for " + quicConnectionTag() + " - local address: " + + quicConnection.localAddress()); + } + } + + public String quicConnectionTag() { + return quicConnection.logTag(); + } + + private static boolean shouldUpdateDynamicTable(TableEntry tableEntry) { + if (tableEntry.type() == TableEntry.EntryType.NAME_VALUE) { + return false; + } + return switch (tableEntry.name().toString()) { + case ":authority", "user-agent" -> !tableEntry.value().isEmpty(); + default -> false; + }; + } + + private void lock() { + lock.lock(); + } + + private void unlock() { + lock.unlock(); + } + + /** + * Debug tag used to create the debug logger for this + * HTTP/3 connection instance. + * + * @return a debug tag + */ + String dbgTag() { + return dbgTag; + } + + /** + * Asynchronously create an instance of an HTTP/3 connection, if the + * server has a known HTTP/3 endpoint. + * @param request the first request that will go over this connection + * @param h3client the HTTP/3 client + * @param exchange the exchange for which this connection is created + * @return a completable future that will be completed with a new + * HTTP/3 connection, or {@code null} if no usable HTTP/3 endpoint + * was found, or completed exceptionally if an error occurred + */ + static CompletableFuture createAsync(HttpRequestImpl request, + Http3ClientImpl h3client, + Exchange exchange) { + assert request.secure(); + final HttpConnection connection = HttpConnection.getConnection(request.getAddress(), + h3client.client(), + exchange, + request, + HTTP_3); + var debug = h3client.debug(); + var where = "Http3Connection.createAsync"; + if (!(connection instanceof HttpQuicConnection httpQuicConnection)) { + if (Log.http3()) { + Log.logHttp3("{0}: Connection for {1} #{2} is not an HttpQuicConnection: {3}", + where, request, exchange.multi.id, connection); + } + if (debug.on()) + debug.log("%s: Connection is not an HttpQuicConnection: %s", where, connection); + if (request.isHttp3Only(exchange.version())) { + assert connection == null; + // may happen if the client doesn't support HTTP3 + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "cannot establish exchange to requested origin with HTTP/3")); + } + return MinimalFuture.completedFuture(null); + } + if (debug.on()) { + debug.log("%s: Got HttpQuicConnection: %s", where, connection); + } + if (Log.http3()) { + Log.logHttp3("{0}: Got HttpQuicConnection for {1} #{2} is: {3}", + where, request, exchange.multi.id, connection.label()); + } + + // Expose the underlying connection to the exchange's aborter so it can + // be closed if a timeout occurs. + exchange.connectionAborter.connection(httpQuicConnection); + + return httpQuicConnection.connectAsync(exchange) + .thenCompose(unused -> httpQuicConnection.finishConnect()) + .thenCompose(unused -> checkSSLConfig(httpQuicConnection)) + .thenCompose(notused-> { + CompletableFuture cf = new MinimalFuture<>(); + try { + if (debug.on()) + debug.log("creating Http3Connection for %s", httpQuicConnection); + Http3Connection hc = new Http3Connection(request, h3client, httpQuicConnection); + if (!hc.isFinalStream()) { + exchange.connectionAborter.clear(httpQuicConnection); + cf.complete(hc); + } else { + var io = new IOException("can't reserve first stream"); + if (Log.http3()) { + Log.logHttp3(" Unable to use HTTP/3 connection over {0}: {1}", + hc.quicConnectionTag(), + io); + } + hc.protocolError(io); + cf.complete(null); + } + } catch (Exception e) { + cf.completeExceptionally(e); + } + return cf; } ) + .whenComplete(httpQuicConnection::connectionEstablished); + } + + private static CompletableFuture checkSSLConfig(HttpQuicConnection quic) { + // HTTP/2 checks ALPN here; with HTTP/3, we only offer one ALPN, + // and TLS verifies that it's negotiated. + + // We can examine the negotiated parameters here and possibly fail + // if they are not satisfactory. + return MinimalFuture.completedFuture(null); + } + + HttpQuicConnection connection() { + return connection; + } + + String key() { + return connectionKey; + } + + /** + * Whether the final stream (last stream allowed on a connection), has + * been set. + * + * @return true if the final stream has been set. + */ + boolean isFinalStream() { + return this.finalStream; + } + + /** + * Sets the final stream to be the next stream opened on + * the connection. No other stream will be opened after this. + */ + void setFinalStream() { + this.finalStream = true; + } + + void setFinalStreamAndCloseIfIdle() { + boolean closeNow; + lock(); + try { + setFinalStream(); + closeNow = finalStreamClosed(); + } finally { + unlock(); + } + if (closeNow) close(); + } + + void allowOnlyOneStream() { + lock(); + try { + if (isFinalStream()) return; + this.allowOnlyOneStream = true; + this.finalStream = true; + } finally { + unlock(); + } + } + + boolean isOpen() { + return closedState == 0 && quicConnection.isOpen(); + } + + private IOException checkConnectionError() { + final TerminationCause tc = quicConnection.terminationCause(); + return tc == null ? null : tc.getCloseCause(); + } + + // Used only by tests + CompletableFuture peerSettingsCF() { + return peerSettingsCF; + } + + private boolean reserveStream() { + lock(); + try { + boolean allowStream0 = this.allowOnlyOneStream; + this.allowOnlyOneStream = false; + if (finalStream && !allowStream0) { + return false; + } + reservedStreamCount.incrementAndGet(); + return true; + } finally { + unlock(); + } + } + + CompletableFuture> + createStream(final Exchange exchange) throws IOException { + // check if this connection is closing before initiating this new stream + if (!reserveStream()) { + if (Log.http3()) { + Log.logHttp3("Cannot initiate new stream on connection {0} for exchange {1}", + quicConnectionTag(), exchange); + } + // we didn't create the stream and thus the server hasn't yet processed this request. + // mark the request as unprocessed to allow it to be retried on a different connection. + exchange.markUnprocessedByPeer(); + String message = "cannot initiate additional new streams on chosen connection"; + IOException cause = streamLimitReached + ? new StreamLimitException(HTTP_3, message) + : new IOException(message); + return MinimalFuture.failedFuture(cause); + } + // TODO: this duration is currently "computed" from the request timeout duration. + // this computation needs a bit more thought + final Duration streamLimitIncreaseDuration = exchange.request.timeout() + .map((reqTimeout) -> reqTimeout.dividedBy(2)) + .orElse(Duration.ofMillis(MAX_STREAM_LIMIT_WAIT_TIMEOUT)); + final CompletableFuture bidiStream = + quicConnection.openNewLocalBidiStream(streamLimitIncreaseDuration); + // once the bidi stream creation completes: + // - if completed exceptionally, we transform any QuicStreamLimitException into a + // StreamLimitException + // - if completed successfully, we create a Http3 exchange and return that as the result + final CompletableFuture>> h3ExchangeCf = + bidiStream.handle((stream, t) -> { + if (t == null) { + // no exception occurred and a bidi stream was created on the quic + // connection, but check if the connection has been terminated + // in the meantime + final var terminationCause = checkConnectionError(); + if (terminationCause != null) { + // connection already closed and we haven't yet issued the request. + // mark the exchange as unprocessed to allow it to be retried on + // a different connection. + exchange.markUnprocessedByPeer(); + return MinimalFuture.failedFuture(terminationCause); + } + // creation of bidi stream succeeded, now create the H3 exchange impl + // and return it + final Http3ExchangeImpl h3Exchange = createHttp3ExchangeImpl(exchange, stream); + return MinimalFuture.completedFuture(h3Exchange); + } + // failed to open a bidi stream + reservedStreamCount.decrementAndGet(); + final Throwable cause = Utils.getCompletionCause(t); + if (cause instanceof QuicStreamLimitException) { + if (Log.http3()) { + Log.logHttp3("Maximum stream limit reached on {0} for exchange {1}", + quicConnectionTag(), exchange.multi.streamLimitState()); + } + if (debug.on()) { + debug.log("bidi stream creation failed due to stream limit: " + + cause + ", connection will be marked as unusable for subsequent" + + " requests"); + } + // Since we have reached the stream creation limit (which translates to not + // being able to initiate new requests on this connection), we mark the + // connection as "final stream" (i.e. don't consider this (pooled) + // connection for subsequent requests) + this.streamLimitReachedWith(exchange); + return MinimalFuture.failedFuture(new StreamLimitException(HTTP_3, + "No more streams allowed on connection")); + } else if (cause instanceof ClosedChannelException) { + // stream creation failed due to the connection (that was chosen) + // got closed. Thus the request wasn't processed by the server. + // mark the request as unprocessed to allow it to be + // initiated on a different connection + exchange.markUnprocessedByPeer(); + return MinimalFuture.failedFuture(cause); + } + return MinimalFuture.failedFuture(cause); + }); + return h3ExchangeCf.thenCompose(Function.identity()); + } + + private void streamLimitReachedWith(Exchange exchange) { + streamLimitReached = true; + client.streamLimitReached(this, exchange.request); + setFinalStream(); + } + + private Http3ExchangeImpl createHttp3ExchangeImpl(Exchange exchange, QuicBidiStream stream) { + if (debug.on()) { + debug.log("Temporary reference h3 stream: " + stream.streamId()); + } + if (Log.http3()) { + Log.logHttp3("Creating HTTP/3 exchange for {0}/streamId={1}", + quicConnectionTag(), Long.toString(stream.streamId())); + } + client.client.h3StreamReference(); + try { + lock(); + try { + this.exchangeStreams.put(stream.streamId(), stream); + reservedStreamCount.decrementAndGet(); + var te = idleConnectionTimeoutEvent; + if (te != null) { + client.client().cancelTimer(te); + idleConnectionTimeoutEvent = null; + } + } finally { + unlock(); + } + var http3Exchange = new Http3ExchangeImpl<>(this, exchange, stream); + return registerAndStartExchange(http3Exchange); + } finally { + if (debug.on()) { + debug.log("Temporary unreference h3 stream: " + stream.streamId()); + } + client.client.h3StreamUnreference(); + } + } + + private Http3ExchangeImpl registerAndStartExchange(Http3ExchangeImpl exchange) { + var streamId = exchange.streamId(); + if (debug.on()) debug.log("Reference h3 stream: " + streamId); + client.client.h3StreamReference(); + exchanges.put(streamId, exchange); + exchange.start(); + return exchange; + } + + // marks this connection as no longer available for creating additional streams. current + // streams will run to completion. marking the connection as gracefully shutdown + // can involve sending the necessary protocol message(s) to the peer. + private void sendGoAway() throws IOException { + if (markSentGoAway()) { + // already sent (either successfully or an attempt was made) GOAWAY, nothing more to do + return; + } + // RFC-9114, section 5.2: Endpoints initiate the graceful shutdown of an HTTP/3 connection + // by sending a GOAWAY frame. + final QuicStreamWriter writer = controlStreamPair.localWriter(); + if (writer != null && quicConnection.isOpen()) { + try { + // We send here the largest pushId for which the peer has + // opened a stream. We won't process pushIds larger than that, and + // we will later cancel any pending push promises anyway. + final long lastProcessedPushId = largestPushId.get(); + final GoAwayFrame goAwayFrame = new GoAwayFrame(lastProcessedPushId); + final long size = goAwayFrame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + final var buf = ByteBuffer.allocate((int) size); + goAwayFrame.writeFrame(buf); + buf.flip(); + if (debug.on()) { + debug.log("Sending GOAWAY frame %s from client connection %s", goAwayFrame, this); + } + writer.scheduleForWriting(buf, false); + } catch (Exception e) { + // ignore - we couldn't send a GOAWAY + if (debug.on()) { + debug.log("Failed to send GOAWAY from client " + this, e); + } + Log.logError("Could not send a GOAWAY from client {0}", this); + Log.logError(e); + } + } + } + + @Override + public void close() { + try { + sendGoAway(); + } catch (IOException ioe) { + // log and ignore the failure + // failure to send a GOAWAY shouldn't prevent closing a connection + if (debug.on()) { + debug.log("failed to send a GOAWAY frame before initiating a close: " + ioe); + } + } + // TODO: ideally we should hava flushForClose() which goes all the way to terminator to flush + // streams and increasing the chances of GOAWAY being sent. + // check RFC-9114, section 5.3 which seems to allow including GOAWAY and CONNECTION_CLOSE + // frames in same packet (optionally) + close(Http3Error.H3_NO_ERROR, "H3 connection closed - no error"); + } + + void close(final Throwable throwable) { + close(H3_INTERNAL_ERROR, null, throwable); + } + + void close(final Http3Error error, final String message) { + if (error != H3_NO_ERROR) { + // construct a ProtocolException representing the connection termination cause + final ProtocolException cause = new ProtocolException(message); + close(error, message, cause); + } else { + close(error, message, null); + } + } + + void close(final Http3Error error, final String logMsg, + final Throwable closeCause) { + if (!markClosed()) { + // already closed, nothing to do + return; + } + if (debug.on()) { + debug.log("Closing HTTP/3 connection: %s %s %s", error, logMsg == null ? "" : logMsg, + closeCause == null ? "" : closeCause.toString()); + debug.log("State is: " + describeClosedState(closedState)); + } + exchanges.values().forEach(e -> e.recordError(closeCause)); + // close the underlying QUIC connection + connection.close(error.code(), logMsg, closeCause); + final TerminationCause tc = connection.quicConnection.terminationCause(); + assert tc != null : "termination cause is null"; + // close all HTTP streams + exchanges.values().forEach(exchange -> exchange.cancelImpl(tc.getCloseCause(), error)); + pushManager.cancelAllPromises(tc.getCloseCause(), error); + discardConnectionState(); + // No longer wait for reading HTTP/3 stream types: + // stop waiting on any stream for which we haven't received the stream + // type yet. + try { + var listener = remoteStreamListener; + if (listener != null) { + quicConnection.removeRemoteStreamListener(listener); + } + } finally { + client.connectionClosed(this); + } + if (!peerSettingsCF.isDone()) { + peerSettingsCF.completeExceptionally(tc.getCloseCause()); + } + } + + private void discardConnectionState() { + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + qpackDecoderStreams.stopSchedulers(); + qpackEncoderStreams.stopSchedulers(); + } + + private boolean markClosed() { + return markClosedState(CLOSED); + } + + void protocolError(IOException error) { + connectionError(error, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + void connectionError(Throwable throwable, Http3Error error) { + connectionError(null, throwable, error.code(), null); + } + + void connectionError(Http3Stream exchange, Throwable throwable, long errorCode, + String logMsg) { + final Optional error = Http3Error.fromCode(errorCode); + assert error.isPresent() : "not a HTTP3 error code: " + errorCode; + close(error.get(), logMsg, throwable); + } + + public String toString() { + return String.format("Http3Connection(%s)", connection()); + } + + private boolean finalStreamClosed() { + lock(); + try { + return this.finalStream && this.exchangeStreams.isEmpty() && this.reservedStreamCount.get() == 0; + } finally { + unlock(); + } + } + + /** + * Called by the {@link Http3ExchangeImpl} when the exchange is closed. + * + * @param streamId The request stream id + */ + void onExchangeClose(Http3ExchangeImpl exch, final long streamId) { + // we expect it to be a request/response stream + if (!(QuicStreams.isClientInitiated(streamId) && QuicStreams.isBidirectional(streamId))) { + throw new IllegalArgumentException("Not a client initiated bidirectional stream"); + } + if (this.exchangeStreams.remove(streamId) != null) { + if (connection().quicConnection().isOpen()) { + qpackDecoder.cancelStream(streamId); + } + decrementStreamsCount(exch, streamId); + exchanges.remove(streamId); + } + + if (finalStreamClosed()) { + // no more streams open on this connection. close the connection + if (Log.http3()) { + Log.logHttp3("Closing HTTP/3 connection {0} on final stream (streamId={1})", + quicConnectionTag(), Long.toString(streamId)); + } + // close will take care of canceling all pending push promises + // if any push promises are left pending + close(); + } else { + if (Log.http3()) { + Log.logHttp3("HTTP/3 connection {0} left open: exchanged streamId={1} closed; " + + "finalStream={2}, exchangeStreams={3}, reservedStreamCount={4}", + quicConnectionTag(), Long.toString(streamId), finalStream, + exchangeStreams.size(), reservedStreamCount.get()); + } + lock(); + try { + var te = idleConnectionTimeoutEvent; + if (te == null && exchangeStreams.isEmpty()) { + te = idleConnectionTimeoutEvent = client.client().idleConnectionTimeout(HTTP_3) + .map(IdleConnectionTimeoutEvent::new).orElse(null); + if (te != null) { + client.client().registerTimer(te); + } + } + } finally { + unlock(); + } + } + } + + void decrementStreamsCount(Http3ExchangeImpl exch, long streamid) { + if (exch.deRegister()) { + debug.log("Unreference h3 stream: " + streamid); + client.client.h3StreamUnreference(); + } else { + debug.log("Already unreferenced h3 stream: " + streamid); + } + } + + // Called from Http3PushPromiseStream::start (via Http3ExchangeImpl) + void onPushPromiseStreamStarted(Http3PushPromiseStream http3PushPromiseStream, long streamId) { + // HTTP/3 push promises are not refcounted. + // At the moment an ongoing push promise will not prevent the client + // to exit normally, if all request-response streams are finished. + // Here would be the place to increment ref-counting if we wanted to + } + + // Called by Http3PushPromiseStream::close + void onPushPromiseStreamClosed(Http3PushPromiseStream http3PushPromiseStream, long streamId) { + // HTTP/3 push promises are not refcounted. + // At the moment an ongoing push promise will not prevent the client + // to exit normally, if all request-response streams are finished. + // Here would be the place to decrement ref-counting if we wanted to + if (connection().quicConnection().isOpen()) { + qpackDecoder.cancelStream(streamId); + } + } + + /** + * A class used to dispatch peer initiated unidirectional streams + * according to their HTTP/3 stream type. + * The type of an HTTP/3 unidirectional stream is determined by + * reading a variable length integer code off the stream, which + * indicates the type of stream. + * @see Http3Streams + */ + private final class Http3StreamDispatcher extends PeerUniStreamDispatcher { + Http3StreamDispatcher(QuicReceiverStream stream) { + super(stream); + } + + @Override + protected Logger debug() { return debug; } + + @Override + protected void onStreamAbandoned(QuicReceiverStream stream) { + if (debug.on()) debug.log("Stream " + stream.streamId() + " abandoned!"); + qpackDecoder.cancelStream(stream.streamId()); + } + + @Override + protected void onControlStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, controlStreamPair.futureReceiverStream()); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, qpackDecoderStreams.futureReceiverStream()); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, qpackEncoderStreams.futureReceiverStream()); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + Http3Connection.this.onPushStreamCreated(stream, pushId); + } + + // completes the given completable future with the given stream + private void complete(String description, QuicReceiverStream stream, CompletableFuture cf) { + debug.log("completing CF for %s with stream %s", description, stream.streamId()); + boolean completed = cf.complete(stream); + if (!completed) { + if (!cf.isCompletedExceptionally()) { + debug.log("CF for %s already completed with stream %s!", description, cf.resultNow().streamId()); + close(Http3Error.H3_STREAM_CREATION_ERROR, + "%s already created".formatted(description)); + } else { + debug.log("CF for %s already completed exceptionally!", description); + } + } + } + + /** + * Dispatches the given remote initiated unidirectional stream to the + * given Http3Connection after reading the stream type off the stream. + * + * @param conn the Http3Connection with which the stream is associated + * @param stream a newly opened remote unidirectional stream. + */ + static CompletableFuture dispatch(Http3Connection conn, QuicReceiverStream stream) { + assert stream.isRemoteInitiated(); + assert !stream.isBidirectional(); + var dispatcher = conn.new Http3StreamDispatcher(stream); + dispatcher.start(); + return dispatcher.dispatchCF(); + } + } + + /** + * Attempts to notify the idle connection management that this connection should + * be considered "in use". This way the idle connection management doesn't close + * this connection during the time the connection is handed out from the pool and any + * new stream created on that connection. + * + * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false + * otherwise; in which case the connection must not be handed out from the pool. + */ + boolean tryReserveForPoolCheckout() { + // must be done with "stateLock" held to co-ordinate idle connection management + lock(); + try { + cancelIdleShutdownEvent(); + // co-ordinate with the QUIC connection to prevent it from silently terminating + // a potentially idle transport + if (!quicConnection.connectionTerminator().tryReserveForUse()) { + // QUIC says the connection can't be used + return false; + } + // consider the reservation successful only if the connection's state hasn't moved + // to "being closed" + return isOpen() && finalStream == false; + } finally { + unlock(); + } + } + + /** + * Cancels any event that might have been scheduled to shutdown this connection. Must be called + * with the stateLock held. + */ + private void cancelIdleShutdownEvent() { + assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock; + if (idleConnectionTimeoutEvent == null) return; + idleConnectionTimeoutEvent.cancel(); + idleConnectionTimeoutEvent = null; + } + + // An Idle connection is one that has no active streams + // and has not sent the final stream flag + final class IdleConnectionTimeoutEvent extends TimeoutEvent { + + // both cancelled and idleShutDownInitiated are to be accessed + // when holding the connection's lock + private boolean cancelled; + private boolean idleShutDownInitiated; + + IdleConnectionTimeoutEvent(Duration duration) { + super(duration); + } + + @Override + public void handle() { + boolean okToIdleTimeout; + lock(); + try { + if (cancelled || idleShutDownInitiated) { + return; + } + idleShutDownInitiated = true; + if (debug.on()) { + debug.log("H3 idle shutdown initiated"); + } + setFinalStream(); + okToIdleTimeout = finalStreamClosed(); + } finally { + unlock(); + } + if (okToIdleTimeout) { + if (debug.on()) { + debug.log("closing idle H3 connection"); + } + close(); + } + } + + /** + * Cancels this event. Should be called with stateLock held + */ + void cancel() { + assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock; + // mark as cancelled to prevent potentially already triggered event from actually + // doing the shutdown + this.cancelled = true; + // cancel the timer to prevent the event from being triggered (if it hasn't already) + client.client().cancelTimer(this); + } + + @Override + public String toString() { + return "IdleConnectionTimeoutEvent, " + super.toString(); + } + + } + + /** + * This method is called when the peer opens a new stream. + * The stream can be unidirectional or bidirectional. + * + * @param stream the new stream + * @return always returns true (see {@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + private boolean onOpenRemoteStream(QuicReceiverStream stream) { + debug.log("on open remote stream: " + stream.streamId()); + if (stream instanceof QuicBidiStream bidi) { + // A server will never open a bidirectional stream + // with the client. A client opens a new bidirectional + // stream for each request/response exchange. + return onRemoteBidirectionalStream(bidi); + } else { + // Four types of unidirectional stream are defined: + // control stream, qpack encoder, qpack decoder, push + // promise stream + return onRemoteUnidirectionalStream(stream); + } + } + + /** + * This method is called when the peer opens a unidirectional stream. + * + * @param uni the unidirectional stream opened by the peer + * @return always returns true ({@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + protected boolean onRemoteUnidirectionalStream(QuicReceiverStream uni) { + assert !uni.isBidirectional(); + assert uni.isRemoteInitiated(); + if (!isOpen()) return false; + debug.log("dispatching unidirectional remote stream: " + uni.streamId()); + Http3StreamDispatcher.dispatch(this, uni).whenComplete((r, t)-> { + if (t!=null) this.dispatchingFailed(uni, t); + }); + return true; + } + + /** + * Called when the peer opens a bidirectional stream. + * On the client side, this method should never be called. + * + * @param bidi the new bidirectional stream opened by the + * peer. + * @return always returns false ({@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + protected boolean onRemoteBidirectionalStream(QuicBidiStream bidi) { + assert bidi.isRemoteInitiated(); + assert bidi.isBidirectional(); + + // From RFC 9114, Section 6.1: + // Clients MUST treat receipt of a server-initiated bidirectional + // stream as a connection error of type H3_STREAM_CREATION_ERROR + // [ unless such an extension has been negotiated]. + // We don't support any extension, so this is a connection error. + close(Http3Error.H3_STREAM_CREATION_ERROR, + "Bidirectional stream %s opened by server peer" + .formatted(bidi.streamId())); + return false; + } + + /** + * Called if the dispatch failed. + * + * @param reason the reason of the failure + */ + protected void dispatchingFailed(QuicReceiverStream uni, Throwable reason) { + debug.log("dispatching failed for streamId=%s: %s", uni.streamId(), reason); + close(H3_STREAM_CREATION_ERROR, "failed to dispatch remote stream " + uni.streamId(), reason); + } + + + /** + * Schedules sending of client settings. + * + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + QuicStreamWriter sendSettings(QuicStreamWriter writer) { + try { + final SettingsFrame settings = QPACK.updateDecoderSettings(SettingsFrame.defaultRFCSettings()); + this.ourSettings = ConnectionSettings.createFrom(settings); + this.qpackDecoder.configure(ourSettings); + if (debug.on()) { + debug.log("Sending client settings %s for connection %s", this.ourSettings, this); + } + long size = settings.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + var buf = ByteBuffer.allocate((int) size); + settings.writeFrame(buf); + buf.flip(); + writer.scheduleForWriting(buf, false); + return writer; + } catch (IOException io) { + throw new CompletionException(io); + } + } + + /** + * Schedules sending of max push id that this (client) connection allows. + * + * @param writer the control stream writer + * @return the {@link QuicStreamWriter} passed as parameter + */ + private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer) { + try { + long maxPushId = pushManager.getMaxPushId(); + if (maxPushId > 0 && maxPushId > maxPushIdSent.get()) { + return sendMaxPushId(writer, maxPushId); + } else { + return writer; + } + } catch (IOException io) { + // will wrap the io exception in CompletionException, + // close the connection, and throw. + throw new CompletionException(io); + } + } + + // local control stream write loop + void lcsWriterLoop() { + // since we do not write much data on the control stream + // we don't check for credit and always directly buffer + // the data to send in the writer. Therefore, there is + // nothing to do in the control stream writer loop. + // + // When more credit is available, check if we need + // to send maxpushid; + if (maxPushIdSent.get() < pushManager.getMaxPushId()) { + var writer = controlStreamPair.localWriter(); + if (writer != null && writer.connected()) { + sendMaxPushId(writer); + } + } + } + + void controlStreamFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Control stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9114, section 6.2.1: + // If either control stream is closed at any point, + // this MUST be treated as a connection error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "control stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since control stream " + stream.mode() + + " failed", throwable); + } + } + close(throwable); + } + + /** + * This method is called to process bytes received on the peer + * control stream. + * + * @param buffer the bytes received + */ + private void processPeerControlBytes(final ByteBuffer buffer) { + debug.log("received server control: %s bytes", buffer.remaining()); + controlFramesDecoder.submit(buffer); + Http3Frame frame; + while ((frame = controlFramesDecoder.poll()) != null) { + final long frameType = frame.type(); + debug.log("server control frame: %s", Http3FrameType.asString(frameType)); + if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + final Http3Error error = Http3Error.fromCode(malformed.getErrorCode()) + .orElse(H3_INTERNAL_ERROR); + close(error, malformed.getMessage()); + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } + final boolean settingsRcvd = this.settingsFrameReceived; + if ((frameType == SettingsFrame.TYPE && settingsRcvd) + || !this.frameOrderVerifier.allowsProcessing(frame)) { + final String unexpectedFrameType = Http3FrameType.asString(frameType); + // not expected to be arriving now, we either use H3_FRAME_UNEXPECTED + // or H3_MISSING_SETTINGS for the connection error, depending on the context. + // + // RFC-9114, section 4.1: Receipt of an invalid sequence of frames MUST be + // treated as a connection error of type H3_FRAME_UNEXPECTED. + // + // RFC-9114, section 6.2.1: If the first frame of the control stream + // is any other frame type, this MUST be treated as a connection error of + // type H3_MISSING_SETTINGS. + final String logMsg = "unexpected (order of) frame type: " + unexpectedFrameType + + " on control stream"; + if (!settingsRcvd) { + close(Http3Error.H3_MISSING_SETTINGS, logMsg); + } else { + close(Http3Error.H3_FRAME_UNEXPECTED, logMsg); + } + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } + if (frame instanceof SettingsFrame settingsFrame) { + this.settingsFrameReceived = true; + this.peerSettings = ConnectionSettings.createFrom(settingsFrame); + if (debug.on()) { + debug.log("Received peer settings %s for connection %s", this.peerSettings, this); + } + peerSettingsCF.completeAsync(() -> peerSettings, + client.client().theExecutor().safeDelegate()); + // We can only initialize encoder's DT only when we get Settings frame with all parameters + qpackEncoder().configure(peerSettings); + } + if (frame instanceof CancelPushFrame cancelPush) { + pushManager.cancelPushPromise(cancelPush.getPushId(), null, CancelPushReason.CANCEL_RECEIVED); + } + if (frame instanceof GoAwayFrame goaway) { + handleIncomingGoAway(goaway); + } + if (frame instanceof PartialFrame partial) { + var payloadBytes = controlFramesDecoder.readPayloadBytes(); + debug.log("added %s bytes to %s", + payloadBytes == null ? 0 : Utils.remaining(payloadBytes), + frame); + if (partial.remaining() == 0) { + this.frameOrderVerifier.completed(frame); + } else if (payloadBytes == null || payloadBytes.isEmpty()) { + break; + } + // only reserved frames reach here; just drop the payload + } else { + this.frameOrderVerifier.completed(frame); + } + if (controlFramesDecoder.eof()) { + break; + } + } + if (controlFramesDecoder.eof()) { + close(H3_CLOSED_CRITICAL_STREAM, "EOF reached while reading server control stream"); + } + } + + /** + * Called when a new push promise stream is created by the peer. + * + * @apiNote this method gives an opportunity to cancel the stream + * before reading the pushId, if it is known that no push + * will be accepted anyway. + * + * @param pushStream the new push promise stream + * @param pushId or -1 if the pushId is not available yet + */ + private void onPushStreamCreated(QuicReceiverStream pushStream, long pushId) { + assert pushStream.isRemoteInitiated(); + assert !pushStream.isBidirectional(); + + onPushPromiseStream(pushStream, pushId); + } + + /** + * Called when a new push promise stream is created by the peer, and + * the pushId has been read. + * + * @param pushStream the new push promise stream + * @param pushId the pushId + */ + void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) { + assert pushId >= 0; + pushManager.onPushPromiseStream(pushStream, pushId); + } + + /** + * This method is called by the {@link Http3PushManager} to figure out whether + * a push stream or a push promise should be processed, with respect to the + * GOAWAY state. Any pushId larger than what was sent in the GOAWAY frame + * should be cancelled /rejected. + * + * @param pushStream a push stream (may be null if not yet materialized) + * @param pushId a pushId, must be > 0 + * @return true if the pushId can be processed + */ + boolean acceptLargerPushPromise(QuicReceiverStream pushStream, long pushId) { + // if GOAWAY has been sent, just cancel the push promise + // otherwise - track this as the maxPushId that will be + // sent in GOAWAY + if (checkMaxPushId(pushId) != null) return false; // connection will be closed + while (true) { + long largestPushId = this.largestPushId.get(); + if ((closedState & GOAWAY_SENT) == GOAWAY_SENT) { + if (pushId >= largestPushId) { + if (pushStream != null) { + pushStream.requestStopSending(H3_NO_ERROR.code()); + } + pushManager.cancelPushPromise(pushId, null, CancelPushReason.PUSH_CANCELLED); + return false; + } + } + if (pushId <= largestPushId) break; + if (!this.largestPushId.compareAndSet(largestPushId, pushId)) continue; + if ((closedState & GOAWAY_SENT) == 0) break; + } + // If we reach here, then either GOAWAY has been sent with a largestPushId >= pushId, + // or GOAWAY has not been sent yet. + return true; + } + + QueuingStreamPair createEncoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_ENCODER, quicConnection, + encoderReceiver, this::onEncoderStreamsFailed, debug); + } + + private void onEncoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Encoder stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "QPACK encoder stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since QPack encoder stream " + stream.streamId() + + " failed", throwable); + } + } + close(throwable); + } + + QueuingStreamPair createDecoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_DECODER, quicConnection, + encoderReceiver, this::onDecoderStreamsFailed, debug); + } + + private void onDecoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Decoder stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "QPACK decoder stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since QPack decoder stream " + stream.streamId() + + " failed", throwable); + } + } + close(throwable); + } + + // This method never returns anything: it always throws + private T exceptionallyAndClose(Throwable t) { + try { + return exceptionally(t); + } finally { + close(t); + } + } + + // This method never returns anything: it always throws + private T exceptionally(Throwable t) { + try { + debug.log(t.getMessage(), t); + throw t; + } catch (RuntimeException | Error r) { + throw r; + } catch (ExecutionException x) { + throw new CompletionException(x.getMessage(), x.getCause()); + } catch (Throwable e) { + throw new CompletionException(e.getMessage(), e); + } + } + + Decoder qpackDecoder() { + return qpackDecoder; + } + + Encoder qpackEncoder() { + return qpackEncoder; + } + + /** + * {@return the settings, sent by the peer, for this connection. If none is present, due to the SETTINGS + * frame not yet arriving from the peer, this method returns {@link Optional#empty()}} + */ + Optional getPeerSettings() { + return Optional.ofNullable(this.peerSettings); + } + + private void handleIncomingGoAway(final GoAwayFrame incomingGoAway) { + final long quicStreamId = incomingGoAway.getTargetId(); + if (debug.on()) { + debug.log("Received GOAWAY %s", incomingGoAway); + } + // ensure request stream id is a bidirectional stream originating from the client. + // RFC-9114, section 7.2.6: A client MUST treat receipt of a GOAWAY frame containing + // a stream ID of any other type as a connection error of type H3_ID_ERROR. + if (!(QuicStreams.isClientInitiated(quicStreamId) + && QuicStreams.isBidirectional(quicStreamId))) { + close(Http3Error.H3_ID_ERROR, "Invalid stream id in GOAWAY frame"); + return; + } + boolean validStreamId = false; + long current = lowestGoAwayReceipt.get(); + while (current == -1 || quicStreamId <= current) { + if (lowestGoAwayReceipt.compareAndSet(current, quicStreamId)) { + validStreamId = true; + break; + } + current = lowestGoAwayReceipt.get(); + } + if (!validStreamId) { + // the request stream id received in the GOAWAY frame is greater than the one received + // in some previous GOAWAY frame. This isn't allowed by spec. + // RFC-9114, section 5.2: An endpoint MAY send multiple GOAWAY frames indicating + // different identifiers, but the identifier in each frame MUST NOT be greater than + // the identifier in any previous frame, ... Receiving a GOAWAY containing a larger + // identifier than previously received MUST be treated as a connection error of + // type H3_ID_ERROR. + close(Http3Error.H3_ID_ERROR, "Invalid stream id in newer GOAWAY frame"); + return; + } + markReceivedGoAway(); + // mark a state on this connection to let it know that no new streams are allowed on this + // connection. + // RFC-9114, section 5.2: Endpoints MUST NOT initiate new requests or promise new pushes on + // the connection after receipt of a GOAWAY frame from the peer. + setFinalStream(); + if (debug.on()) { + debug.log("Connection will no longer allow new streams due to receipt of GOAWAY" + + " from peer"); + } + handlePeerUnprocessedStreams(quicStreamId); + if (finalStreamClosed()) { + close(Http3Error.H3_NO_ERROR, "GOAWAY received"); + } + } + + private void handlePeerUnprocessedStreams(final long leastUnprocessedStreamId) { + this.exchanges.forEach((id, exchange) -> { + if (id >= leastUnprocessedStreamId) { + // close the exchange as unprocessed + client.client().theExecutor().execute(exchange::closeAsUnprocessed); + } + }); + } + + private boolean isMarked(int state, int mask) { + return (state & mask) == mask; + } + + private boolean markSentGoAway() { + return markClosedState(GOAWAY_SENT); + } + + private boolean markReceivedGoAway() { + return markClosedState(GOAWAY_RECEIVED); + } + + private boolean markClosedState(int flag) { + int state, desired; + do { + state = closedState; + if ((state & flag) == flag) return false; + desired = state | flag; + } while (!CLOSED_STATE.compareAndSet(this, state, desired)); + return true; + } + + String describeClosedState(int state) { + if (state == 0) return "active"; + String desc = null; + if (isMarked(state, GOAWAY_SENT)) { + if (desc == null) desc = "goaway-sent"; + else desc += "+goaway-sent"; + } + if (isMarked(state, GOAWAY_RECEIVED)) { + if (desc == null) desc = "goaway-rcvd"; + else desc += "+goaway-rcvd"; + } + if (isMarked(state, CLOSED)) { + if (desc == null) desc = "quic-closed"; + else desc += "+quic-closed"; + } + return desc != null ? desc : "0x" + Integer.toHexString(state); + } + + // PushPromise handling + // ==================== + + /** + * {@return a new PushId for the given pushId} + * @param pushId the pushId + */ + PushId newPushId(long pushId) { + return new Http3PushId(pushId, connection.label()); + } + + /** + * Called when a pushId needs to be cancelled. + * @param pushId the pushId to cancel + * @param cause the cause (may be {@code null}). + */ + void pushCancelled(long pushId, Throwable cause) { + pushManager.cancelPushPromise(pushId, cause, CancelPushReason.PUSH_CANCELLED); + } + + /** + * Called if a PushPromiseFrame is received by an exchange that doesn't have any + * {@link java.net.http.HttpResponse.PushPromiseHandler}. The pushId will be + * cancelled, unless it's already been accepted by another exchange. + * + * @param pushId the pushId + */ + void noPushHandlerFor(long pushId) { + pushManager.cancelPushPromise(pushId, null, CancelPushReason.NO_HANDLER); + } + + boolean acceptPromises() { + return exchanges.values().stream().anyMatch(Http3ExchangeImpl::acceptPushPromise); + } + + /** + * {@return a completable future that will be completed when a pushId has been + * accepted by the exchange in charge of creating the response body} + *

    + * The completable future is complete with {@code true} if the pushId is + * accepted, and with {@code false} if the pushId was rejected or cancelled. + * + * @apiNote + * This method is intended to be called when {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false, + * indicating that the push promise is being delegated to another request/response + * exchange. + * On completion of the future returned here, if the future is completed + * with {@code true}, the caller is expected to call {@link + * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link + * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}. + *

    + * Callers should not forward the pushId to a {@link + * java.net.http.HttpResponse.PushPromiseHandler} unless the future is completed + * with {@code true} + * + * @param pushId the pushId + */ + CompletableFuture whenPushAccepted(long pushId) { + return pushManager.whenAccepted(pushId); + } + + /** + * Called when a PushPromiseFrame has been decoded. + * + * @param exchange The HTTP/3 exchange that received the frame + * @param pushId The pushId contained in the frame + * @param promiseHeaders The push promise headers contained in the frame + * @return true if the exchange should take care of creating the HttpResponse body, + * false otherwise + */ + boolean onPushPromiseFrame(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) + throws IOException { + return pushManager.onPushPromiseFrame(exchange, pushId, promiseHeaders); + } + + /** + * Checks whether a MAX_PUSH_ID frame should be sent. + */ + void checkSendMaxPushId() { + pushManager.checkSendMaxPushId(); + } + + /** + * Schedules sending of max push id that this (client) connection allows. + * + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer, long maxPushId) throws IOException { + debug.log("Sending max push id frame with max push id set to " + maxPushId); + final MaxPushIdFrame maxPushIdFrame = new MaxPushIdFrame(maxPushId); + final long frameSize = maxPushIdFrame.size(); + assert frameSize >= 0 && frameSize < Integer.MAX_VALUE; + final ByteBuffer buf = ByteBuffer.allocate((int) frameSize); + maxPushIdFrame.writeFrame(buf); + buf.flip(); + if (writer.credit() > buf.remaining()) { + long previous; + do { + previous = maxPushIdSent.get(); + if (previous >= maxPushId) return writer; + } while (!maxPushIdSent.compareAndSet(previous, maxPushId)); + writer.scheduleForWriting(buf, false); + } + return writer; + } + + /** + * Send a MAX_PUSH_ID frame on the control stream with the given {@code maxPushId} + * + * @param maxPushId the new maxPushId + * + * @throws IOException if the pushId could not be sent + */ + void sendMaxPushId(long maxPushId) throws IOException { + sendMaxPushId(controlStreamPair.localWriter(), maxPushId); + } + + /** + * Sends a CANCEL_PUSH frame for the given {@code pushId}. + * If not null, the cause may indicate why the push is cancelled. + * + * @apiNote the cause is only used for logging + * + * @param pushId the pushId to cancel + * @param cause the reason for cancelling, may be {@code null} + */ + void sendCancelPush(long pushId, Throwable cause) { + // send CANCEL_PUSH frame here + if (debug.on()) { + if (cause != null) { + debug.log("Push Promise %s cancelled: %s", pushId, cause.getMessage()); + } else { + debug.log("Push Promise %s cancelled", pushId); + } + } + try { + CancelPushFrame cancelPush = new CancelPushFrame(pushId); + long size = cancelPush.size(); + // frame should contain type, length, pushId + assert size <= 3 * VariableLengthEncoder.MAX_INTEGER_LENGTH; + ByteBuffer buffer = ByteBuffer.allocate((int) size); + cancelPush.writeFrame(buffer); + controlStreamPair.localWriter().scheduleForWriting(buffer, false); + } catch (IOException io) { + debug.log("Failed to cancel pushId: " + pushId); + } + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + IOException checkMaxPushId(long pushId) { + return checkMaxPushId(pushId, maxPushIdSent.get()); + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + private IOException checkMaxPushId(long pushId, long max) { + if (pushId >= max) { + var io = new ProtocolException("Max pushId exceeded (%s >= %s)".formatted(pushId, max)); + connectionError(io, Http3Error.H3_ID_ERROR); + return io; + } + return null; + } + + /** + * {@return the minimum pushId that can be accepted from the peer} + * Any pushId strictly less than this value must be ignored. + * + * @apiNote The minimum pushId represents the smallest pushId that + * was recorded in our history. For smaller pushId, no history has + * been kept, due to history size constraints. Any pushId strictly + * less than this value must be ignored. + */ + public long getMinPushId() { + return pushManager.getMinPushId(); + } + + private static final VarHandle CLOSED_STATE; + static { + try { + CLOSED_STATE = MethodHandles.lookup().findVarHandle(Http3Connection.class, "closedState", int.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java new file mode 100644 index 00000000000..eaacd8213cb --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.net.http.common.Logger; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/** + * This class encapsulate the HTTP/3 connection pool managed + * by an instance of {@link Http3ClientImpl}. + */ +class Http3ConnectionPool { + /* Map key is "scheme:host:port" */ + private final Map advertised = new ConcurrentHashMap<>(); + /* Map key is "scheme:host:port" */ + private final Map unadvertised = new ConcurrentHashMap<>(); + + private final Logger debug; + Http3ConnectionPool(Logger logger) { + this.debug = Objects.requireNonNull(logger); + } + + // https:: + String connectionKey(HttpRequestImpl request) { + var uri = request.uri(); + var scheme = uri.getScheme().toLowerCase(Locale.ROOT); + var host = uri.getHost(); + var port = uri.getPort(); + assert scheme.equals("https"); + if (port < 0) port = 443; // https + return String.format("%s:%s:%d", scheme, host, port); + } + + private Http3Connection lookupUnadvertised(String key, Http3DiscoveryMode discoveryMode) { + var unadvertisedConn = unadvertised.get(key); + if (unadvertisedConn == null) return null; + if (discoveryMode == ANY) return unadvertisedConn; + if (discoveryMode == ALT_SVC) return null; + + assert discoveryMode == HTTP_3_URI_ONLY : String.valueOf(discoveryMode); + + // Double check that if there is an alt service, it has same origin. + final var altService = Optional.ofNullable(unadvertisedConn) + .map(Http3Connection::connection) + .flatMap(HttpQuicConnection::getSourceAltService) + .orElse(null); + + if (altService == null || altService.originHasSameAuthority()) { + return unadvertisedConn; + } + + // We should never come here. + assert false : "unadvertised connection with different origin: %s -> %s" + .formatted(key, altService); + return null; + } + + Http3Connection lookupFor(HttpRequestImpl request) { + var discoveryMode = request.http3Discovery(); + var key = connectionKey(request); + + Http3Connection unadvertisedConn = null; + // If not ALT_SVC, we can use unadvertised connections + if (discoveryMode != ALT_SVC) { + unadvertisedConn = lookupUnadvertised(key, discoveryMode); + if (unadvertisedConn != null && discoveryMode == HTTP_3_URI_ONLY) { + if (debug.on()) { + debug.log("Direct HTTP/3 connection found for %s in connection pool %s", + discoveryMode, unadvertisedConn.connection().label()); + } + return unadvertisedConn; + } + } + + // Then see if we have a connection which was advertised. + var advertisedConn = advertised.get(key); + // We can use it for HTTP3_URI_ONLY too if it has same origin + if (advertisedConn != null) { + final var altService = advertisedConn.connection() + .getSourceAltService().orElse(null); + assert altService != null && altService.wasAdvertised(); + switch (discoveryMode) { + case ANY -> { + return advertisedConn; + } + case ALT_SVC -> { + if (debug.on()) { + debug.log("HTTP/3 connection found for %s in connection pool %s", + discoveryMode, advertisedConn.connection().label()); + } + return advertisedConn; + } + case HTTP_3_URI_ONLY -> { + if (altService != null && altService.originHasSameAuthority()) { + if (debug.on()) { + debug.log("Same authority HTTP/3 connection found for %s in connection pool %s", + discoveryMode, advertisedConn.connection().label()); + } + return advertisedConn; + } + } + } + } + + if (unadvertisedConn != null) { + assert discoveryMode != ALT_SVC; + if (debug.on()) { + debug.log("Direct HTTP/3 connection found for %s in connection pool %s", + discoveryMode, unadvertisedConn.connection().label()); + } + return unadvertisedConn; + } + + // do not log here: this produces confusing logs as this method + // can be called several times when trying to establish a + // connection, when no connection is found in the pool + return null; + } + + Http3Connection putIfAbsent(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()); + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + return advertised.putIfAbsent(key, c); + } + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.putIfAbsent(key, c); + } + + Http3Connection put(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()) : "key mismatch %s -> %s" + .formatted(key, c.key()); + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + return advertised.put(key, c); + } + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.put(key, c); + } + + boolean remove(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()) : "key mismatch %s -> %s" + .formatted(key, c.key()); + + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + boolean remUndavertised = unadvertised.remove(key, c); + assert !remUndavertised + : "advertised connection found in unadvertised pool for " + key; + return advertised.remove(key, c); + } + + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.remove(key, c); + } + + void clear() { + advertised.clear(); + unadvertised.clear(); + } + + java.util.stream.Stream values() { + return java.util.stream.Stream.concat( + advertised.values().stream(), + unadvertised.values().stream()); + } + +} + diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java new file mode 100644 index 00000000000..41a4a84958a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java @@ -0,0 +1,1806 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.ProtocolException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiPredicate; + +import jdk.internal.net.http.PushGroup.Acceptor; +import jdk.internal.net.http.common.HttpBodySubscriberWrapper; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SubscriptionBase; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import static jdk.internal.net.http.http3.ConnectionSettings.UNLIMITED_MAX_FIELD_SECTION_SIZE; + +/** + * This class represents an HTTP/3 Request/Response stream. + */ +final class Http3ExchangeImpl extends Http3Stream { + + private static final String COOKIE_HEADER = "Cookie"; + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3Connection connection; + private final HttpRequestImpl request; + private final BodyPublisher requestPublisher; + private final HttpHeadersBuilder responseHeadersBuilder; + private final HeadersConsumer rspHeadersConsumer; + private final HttpHeaders requestPseudoHeaders; + private final HeaderFrameReader headerFrameReader; + private final HeaderFrameWriter headerFrameWriter; + private final Decoder qpackDecoder; + private final Encoder qpackEncoder; + private final AtomicReference errorRef; + private final CompletableFuture requestBodyCF; + + private final FramesDecoder framesDecoder = + new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnRequestStream); + private final SequentialScheduler readScheduler = + SequentialScheduler.lockingScheduler(this::processQuicData); + private final SequentialScheduler writeScheduler = + SequentialScheduler.lockingScheduler(this::sendQuicData); + private final List> response_cfs = new ArrayList<>(5); + private final ReentrantLock stateLock = new ReentrantLock(); + private final ReentrantLock response_cfs_lock = new ReentrantLock(); + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForRequestResponseStream(); + + + final SubscriptionBase userSubscription = + new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError); + + private final QuicBidiStream stream; + private final QuicStreamReader reader; + private final QuicStreamWriter writer; + volatile boolean closed; + volatile RequestSubscriber requestSubscriber; + volatile HttpResponse.BodySubscriber pendingResponseSubscriber; + volatile HttpResponse.BodySubscriber responseSubscriber; + volatile CompletableFuture responseBodyCF; + volatile boolean requestSent; + volatile boolean responseReceived; + volatile long requestContentLen; + volatile int responseCode; + volatile Response response; + volatile boolean stopRequested; + volatile boolean deRegistered; + private String dbgTag = null; + private final AtomicLong sentQuicBytes = new AtomicLong(); + + Http3ExchangeImpl(final Http3Connection connection, final Exchange exchange, + final QuicBidiStream stream) { + super(exchange); + this.errorRef = new AtomicReference<>(); + this.requestBodyCF = new MinimalFuture<>(); + this.connection = connection; + this.request = exchange.request(); + this.requestPublisher = request.requestPublisher; // may be null + this.responseHeadersBuilder = new HttpHeadersBuilder(); + this.rspHeadersConsumer = new HeadersConsumer(ValidatingHeadersConsumer.Context.RESPONSE); + this.qpackDecoder = connection.qpackDecoder(); + this.qpackEncoder = connection.qpackEncoder(); + this.headerFrameReader = qpackDecoder.newHeaderFrameReader(rspHeadersConsumer); + this.headerFrameWriter = qpackEncoder.newHeaderFrameWriter(); + this.requestPseudoHeaders = Utils.createPseudoHeaders(request); + this.stream = stream; + this.reader = stream.connectReader(readScheduler); + this.writer = stream.connectWriter(writeScheduler); + if (debug.on()) debug.log("Http3ExchangeImpl created"); + } + + public void start() { + if (exchange.pushGroup != null) { + connection.checkSendMaxPushId(); + } + if (Log.http3()) { + Log.logHttp3("Starting HTTP/3 exchange for {0}/streamId={1} ({2} #{3})", + connection.quicConnectionTag(), Long.toString(stream.streamId()), + request, Long.toString(exchange.multi.id)); + } + this.reader.start(); + } + + boolean acceptPushPromise() { + return exchange.pushGroup != null; + } + + String dbgTag() { + if (dbgTag != null) return dbgTag; + long streamId = streamId(); + String sid = streamId == -1 ? "?" : String.valueOf(streamId); + String ctag = connection == null ? null : connection.dbgTag(); + String tag = "Http3ExchangeImpl(" + ctag + ", streamId=" + sid + ")"; + if (streamId == -1) return tag; + return dbgTag = tag; + } + + @Override + long streamId() { + var stream = this.stream; + return stream == null ? -1 : stream.streamId(); + } + + Http3Connection http3Connection() { + return connection; + } + + void recordError(Throwable closeCause) { + errorRef.compareAndSet(null, closeCause); + } + + private sealed class HeadersConsumer extends StreamHeadersConsumer permits PushHeadersConsumer { + + private HeadersConsumer(Context context) { + super(context); + } + + @Override + protected HeaderFrameReader headerFrameReader() { + return headerFrameReader; + } + + @Override + protected HttpHeadersBuilder headersBuilder() { + return responseHeadersBuilder; + } + + @Override + protected final Decoder qpackDecoder() { + return qpackDecoder; + } + + void resetDone() { + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + + @Override + String headerFieldType() { + return "RESPONSE HEADER FIELD"; + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed response: " + super.formatMessage(message, header); + } + + @Override + protected void headersCompleted() { + handleResponse(); + } + + @Override + public final long streamId() { + return stream.streamId(); + } + + } + + private final class PushHeadersConsumer extends HeadersConsumer { + volatile PushPromiseState state; + + private PushHeadersConsumer() { + super(Context.REQUEST); + } + + @Override + protected HttpHeadersBuilder headersBuilder() { + return state.headersBuilder(); + } + + @Override + protected HeaderFrameReader headerFrameReader() { + return state.reader(); + } + + @Override + String headerFieldType() { + return "PUSH REQUEST HEADER FIELD"; + } + + void resetDone() { + if (debug.on()) { + debug.log("Push request builder cleared."); + } + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed push request: " + super.formatMessage(message, header); + } + + @Override + protected void headersCompleted() { + try { + if (exchange.pushGroup == null) { + long pushId = state.frame().getPushId(); + connection.noPushHandlerFor(pushId); + reset(); + } else { + handlePromise(this); + } + } catch (IOException io) { + cancelPushPromise(state, io); + } + } + + public void setState(PushPromiseState state) { + this.state = state; + } + } + + // TODO: this is also defined on Stream + // + private static boolean hasProxyAuthorization(HttpHeaders headers) { + return headers.firstValue("proxy-authorization") + .isPresent(); + } + + // TODO: this is also defined on Stream + // + // Determines whether we need to build a new HttpHeader object. + // + // Ideally we should pass the filter to OutgoingHeaders refactor the + // code that creates the HeaderFrame to honor the filter. + // We're not there yet - so depending on the filter we need to + // apply and the content of the header we will try to determine + // whether anything might need to be filtered. + // If nothing needs filtering then we can just use the + // original headers. + private static boolean needsFiltering(HttpHeaders headers, + BiPredicate filter) { + if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) { + // we're either connecting or proxying + // slight optimization: we only need to filter out + // disabled schemes, so if there are none just + // pass through. + return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER) + && hasProxyAuthorization(headers); + } else { + // we're talking to a server, either directly or through + // a tunnel. + // Slight optimization: we only need to filter out + // proxy authorization headers, so if there are none just + // pass through. + return hasProxyAuthorization(headers); + } + } + + // TODO: this is also defined on Stream + // + private HttpHeaders filterHeaders(HttpHeaders headers) { + HttpConnection conn = connection(); + BiPredicate filter = conn.headerFilter(request); + if (needsFiltering(headers, filter)) { + return HttpHeaders.of(headers.map(), filter); + } + return headers; + } + + @Override + HttpQuicConnection connection() { + return connection.connection(); + } + + @Override + CompletableFuture> sendHeadersAsync() { + final MinimalFuture completable = MinimalFuture.completedFuture(null); + return completable.thenApply(_ -> this.sendHeaders()); + } + + private Http3ExchangeImpl sendHeaders() { + assert stream != null; + assert writer != null; + + if (debug.on()) debug.log("H3 sendHeaders"); + if (Log.requests()) { + Log.logRequest(request.toString()); + } + if (requestPublisher != null) { + requestContentLen = requestPublisher.contentLength(); + } else { + requestContentLen = 0; + } + + Throwable t = errorRef.get(); + if (t != null) { + if (debug.on()) debug.log("H3 stream already cancelled, headers not sent: %s", (Object) t); + if (t instanceof CompletionException ce) throw ce; + throw new CompletionException(t); + } + + HttpHeadersBuilder h = request.getSystemHeadersBuilder(); + if (requestContentLen > 0) { + h.setHeader("content-length", Long.toString(requestContentLen)); + } + HttpHeaders sysh = filterHeaders(h.build()); + HttpHeaders userh = filterHeaders(request.getUserHeaders()); + // Filter context restricted from userHeaders + userh = HttpHeaders.of(userh.map(), Utils.ACCEPT_ALL); + Utils.setUserAuthFlags(request, userh); + + // Don't override Cookie values that have been set by the CookieHandler. + final HttpHeaders uh = userh; + BiPredicate overrides = + (k, v) -> COOKIE_HEADER.equalsIgnoreCase(k) + || uh.firstValue(k).isEmpty(); + + // Filter any headers from systemHeaders that are set in userHeaders + // except for "Cookie:" - user cookies will be appended to system + // cookies + sysh = HttpHeaders.of(sysh.map(), overrides); + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("H3 HEADERS FRAME (stream="); + sb.append(streamId()).append(")\n"); + Log.dumpHeaders(sb, " ", requestPseudoHeaders); + Log.dumpHeaders(sb, " ", sysh); + Log.dumpHeaders(sb, " ", userh); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + final Optional peerSettings = connection.getPeerSettings(); + // It's possible that the peer settings hasn't yet arrived, in which case we use the + // default of "unlimited" header size limit and proceed with sending the request. As per + // RFC-9114, section 7.2.4.2, this is allowed: All settings begin at an initial value. Each + // endpoint SHOULD use these initial values to send messages before the peer's SETTINGS frame + // has arrived, as packets carrying the settings can be lost or delayed. + // When the SETTINGS frame arrives, any settings are changed to their new values. This + // removes the need to wait for the SETTINGS frame before sending messages. + final long headerSizeLimit = peerSettings.isEmpty() ? UNLIMITED_MAX_FIELD_SECTION_SIZE + : peerSettings.get().maxFieldSectionSize(); + if (headerSizeLimit != UNLIMITED_MAX_FIELD_SECTION_SIZE) { + // specific limit has been set on the header size for this connection. + // we compute the header size and ensure that it doesn't exceed that limit + final long computedHeaderSize = computeHeaderSize(requestPseudoHeaders, sysh, userh); + if (computedHeaderSize > headerSizeLimit) { + // RFC-9114, section 4.2.2: An implementation that has received this parameter + // SHOULD NOT send an HTTP message header that exceeds the indicated size. + // we fail the request. + throw new CompletionException(new ProtocolException("Request headers size" + + " exceeds limit set by peer")); + } + } + List buffers = qpackEncoder.encodeHeaders(headerFrameWriter, streamId(), + 1024, requestPseudoHeaders, sysh, userh); + HeadersFrame headersFrame = new HeadersFrame(Utils.remaining(buffers)); + ByteBuffer buffer = ByteBuffer.allocate(headersFrame.headersSize()); + headersFrame.writeHeaders(buffer); + buffer.flip(); + long sentBytes = 0; + try { + boolean hasNoBody = requestContentLen == 0; + int last = buffers.size() - 1; + int toSend = buffer.remaining(); + if (last < 0) { + writer.scheduleForWriting(buffer, hasNoBody); + } else { + writer.queueForWriting(buffer); + } + sentBytes += toSend; + for (int i = 0; i <= last; i++) { + var nextBuffer = buffers.get(i); + toSend = nextBuffer.remaining(); + if (i == last) { + writer.scheduleForWriting(nextBuffer, hasNoBody); + } else { + writer.queueForWriting(nextBuffer); + } + sentBytes += toSend; + } + } catch (QPackException qe) { + if (qe.isConnectionError()) { + // close the connection + connection.close(qe.http3Error(), "QPack error", qe.getCause()); + } + // fail the request + throw new CompletionException(qe.getCause()); + } catch (IOException io) { + throw new CompletionException(io); + } finally { + if (sentBytes != 0) sentQuicBytes.addAndGet(sentBytes); + } + return this; + } + + private static long computeHeaderSize(final HttpHeaders... headers) { + // RFC-9114, section 4.2.2 states: The size of a field list is calculated based on + // the uncompressed size of fields, including the length of the name and value in bytes + // plus an overhead of 32 bytes for each field. + final int OVERHEAD_BYTES_PER_FIELD = 32; + long computedHeaderSize = 0; + for (final HttpHeaders h : headers) { + for (final Map.Entry> entry : h.map().entrySet()) { + try { + computedHeaderSize = Math.addExact(computedHeaderSize, + entry.getKey().getBytes(StandardCharsets.US_ASCII).length); + for (final String v : entry.getValue()) { + computedHeaderSize = Math.addExact(computedHeaderSize, + v.getBytes(StandardCharsets.US_ASCII).length); + } + computedHeaderSize = Math.addExact(computedHeaderSize, OVERHEAD_BYTES_PER_FIELD); + } catch (ArithmeticException ae) { + // overflow, no point trying to compute further, return MAX_VALUE + return Long.MAX_VALUE; + } + } + } + return computedHeaderSize; + } + + + @Override + CompletableFuture> sendBodyAsync() { + return sendBodyImpl().thenApply((e) -> this); + } + + CompletableFuture sendBodyImpl() { + requestBodyCF.whenComplete((v, t) -> requestSent()); + try { + if (debug.on()) debug.log("H3 sendBodyImpl"); + if (requestPublisher != null && requestContentLen != 0) { + final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen); + requestPublisher.subscribe(requestSubscriber = subscriber); + } else { + // there is no request body, therefore the request is complete, + // END_STREAM has already sent with outgoing headers + requestBodyCF.complete(null); + } + } catch (Throwable t) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + requestBodyCF.completeExceptionally(t); + } + return requestBodyCF; + } + + // The Http3StreamResponseSubscriber is registered with the HttpClient + // to ensure that it gets completed if the SelectorManager aborts due + // to unexpected exceptions. + private void registerResponseSubscriber(Http3StreamResponseSubscriber subscriber) { + if (client().registerSubscriber(subscriber)) { + if (debug.on()) { + debug.log("Reference response body for h3 stream: " + streamId()); + } + client().h3StreamReference(); + } + } + + private void unregisterResponseSubscriber(Http3StreamResponseSubscriber subscriber) { + if (client().unregisterSubscriber(subscriber)) { + if (debug.on()) { + debug.log("Unreference response body for h3 stream: " + streamId()); + } + client().h3StreamUnreference(); + } + } + + final class Http3StreamResponseSubscriber extends HttpBodySubscriberWrapper { + Http3StreamResponseSubscriber(BodySubscriber subscriber) { + super(subscriber); + } + + @Override + protected void unregister() { + unregisterResponseSubscriber(this); + } + + @Override + protected void register() { + registerResponseSubscriber(this); + } + + @Override + protected void logComplete(Throwable error) { + if (error == null) { + if (Log.requests()) { + Log.logResponse(() -> "HTTP/3 body successfully completed for: " + request + + " #" + exchange.multi.id); + } + } else { + if (Log.requests()) { + Log.logResponse(() -> "HTTP/3 body exceptionally completed for: " + + request + " (" + error + ")" + + " #" + exchange.multi.id); + } + } + } + } + + + @Override + Http3StreamResponseSubscriber createResponseSubscriber(BodyHandler handler, + ResponseInfo response) { + if (debug.on()) debug.log("Creating body subscriber"); + Http3StreamResponseSubscriber subscriber = + new Http3StreamResponseSubscriber<>(handler.apply(response)); + return subscriber; + } + + @Override + CompletableFuture readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) { + try { + if (Log.trace()) { + Log.logTrace("Reading body on stream {0}", streamId()); + } + if (debug.on()) debug.log("Getting BodySubscriber for: " + response); + Http3StreamResponseSubscriber bodySubscriber = + createResponseSubscriber(handler, new ResponseInfoImpl(response)); + CompletableFuture cf = receiveResponseBody(bodySubscriber, executor); + + PushGroup pg = exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(e)); + } + var bodyCF = cf; + return bodyCF; + } catch (Throwable t) { + // may be thrown by handler.apply + // TODO: Is this the right error code? + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + return MinimalFuture.failedFuture(t); + } + } + + @Override + CompletableFuture ignoreBody() { + try { + if (debug.on()) debug.log("Ignoring body"); + reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return MinimalFuture.completedFuture(null); + } catch (Throwable e) { + if (Log.trace()) { + Log.logTrace("Error requesting stop sending for stream {0}: {1}", + streamId(), e.toString()); + } + return MinimalFuture.failedFuture(e); + } + } + + @Override + void cancel() { + if (debug.on()) debug.log("cancel"); + var stream = this.stream; + if ((stream == null)) { + cancel(new IOException("Stream cancelled before streamid assigned")); + } else { + cancel(new IOException("Stream " + stream.streamId() + " cancelled")); + } + } + + @Override + void cancel(IOException cause) { + cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED); + } + + @Override + void onProtocolError(IOException cause) { + final long streamId = stream.streamId(); + if (debug.on()) { + debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage()); + } + Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause); + cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + @Override + void released() { + long streamid = streamId(); + if (debug.on()) debug.log("Released stream %d", streamid); + // remove this stream from the Http2Connection map. + connection.onExchangeClose(this, streamid); + } + + @Override + void completed() { + } + + @Override + boolean isCanceled() { + return errorRef.get() != null; + } + + @Override + Throwable getCancelCause() { + return errorRef.get(); + } + + @Override + void cancelImpl(Throwable e, Http3Error error) { + try { + var streamid = streamId(); + if (errorRef.compareAndSet(null, e)) { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream", e); + else debug.log("cancelling stream " + streamid + ":", e); + } + if (Log.trace()) { + if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e); + else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e); + } + } else { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", (Object) e); + else debug.log("cancelling stream %s: %s", streamid, e); + } + } + var firstError = errorRef.get(); + completeResponseExceptionally(firstError); + if (!requestBodyCF.isDone()) { + // complete requestBodyCF before cancelling subscription + requestBodyCF.completeExceptionally(firstError); // we may be sending the body... + var requestSubscriber = this.requestSubscriber; + if (requestSubscriber != null) { + cancel(requestSubscriber.subscription.get()); + } + } + var responseBodyCF = this.responseBodyCF; + if (responseBodyCF != null) { + responseBodyCF.completeExceptionally(firstError); + } + // will send a RST_STREAM frame + var stream = this.stream; + if (connection.isOpen()) { + if (stream != null && stream.sendingState().isSending()) { + // no use reset if already closed. + var cause = Utils.getCompletionCause(firstError); + if (!(cause instanceof EOFException)) { + if (debug.on()) + debug.log("sending reset %s", error); + stream.reset(error.code()); + } + } + if (stream != null) { + if (debug.on()) + debug.log("request stop sending"); + stream.requestStopSending(error.code()); + } + } + } catch (Throwable ex) { + errorRef.compareAndSet(null, ex); + if (debug.on()) + debug.log("failed cancelling request: ", ex); + Log.logError(ex); + } finally { + close(); + } + } + + // cancel subscription and ignore errors in order to continue with + // the cancel/close sequence. + private void cancel(Subscription subscription) { + if (subscription == null) return; + try { subscription.cancel(); } + catch (Throwable t) { + debug.log("Unexpected exception thrown by Subscription::cancel", t); + if (Log.errors()) { + Log.logError("Unexpected exception thrown by Subscription::cancel: " + t); + Log.logError(t); + } + } + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + CompletableFuture cf; + // The code below deals with race condition that can be caused when + // completeResponse() is being called before getResponseAsync() + response_cfs_lock.lock(); + try { + if (!response_cfs.isEmpty()) { + // This CompletableFuture was created by completeResponse(). + // it will be already completed, unless the expect continue + // timeout fired + cf = response_cfs.get(0); + if (cf.isDone()) { + cf = response_cfs.remove(0); + } + + // if we find a cf here it should be already completed. + // finding a non completed cf should not happen. just assert it. + assert cf.isDone() || request.expectContinue && expectTimeoutRaised() + : "Removing uncompleted response: could cause code to hang!"; + } else { + // getResponseAsync() is called first. Create a CompletableFuture + // that will be completed by completeResponse() when + // completeResponse() is called. + cf = new MinimalFuture<>(); + response_cfs.add(cf); + } + } finally { + response_cfs_lock.unlock(); + } + if (executor != null && !cf.isDone()) { + // protect from executing later chain of CompletableFuture operations from SelectorManager thread + cf = cf.thenApplyAsync(r -> r, executor); + } + if (Log.trace()) { + Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf); + } + PushGroup pg = exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(Utils.getCompletionCause(e))); + } + if (debug.on()) debug.log("Response future is %s", cf); + return cf; + } + + /** + * Completes the first uncompleted CF on list, and removes it. If there is no + * uncompleted CF then creates one (completes it) and adds to list + */ + void completeResponse(Response resp) { + if (debug.on()) debug.log("completeResponse: %s", resp); + response_cfs_lock.lock(); + try { + CompletableFuture cf; + int cfs_len = response_cfs.size(); + for (int i = 0; i < cfs_len; i++) { + cf = response_cfs.get(i); + if (!cf.isDone() && !expectTimeoutRaised()) { + if (Log.trace()) { + Log.logTrace("Completing response (streamid={0}): {1}", + streamId(), cf); + } + if (debug.on()) + debug.log("Completing responseCF(%d) with response headers", i); + response_cfs.remove(cf); + cf.complete(resp); + return; + } else if (expectTimeoutRaised()) { + Log.logTrace("Completing response (streamid={0}): {1}", + streamId(), cf); + if (debug.on()) + debug.log("Completing responseCF(%d) with response headers", i); + // The Request will be removed in getResponseAsync() + cf.complete(resp); + return; + } // else we found the previous response: just leave it alone. + } + cf = MinimalFuture.completedFuture(resp); + if (Log.trace()) { + Log.logTrace("Created completed future (streamid={0}): {1}", + streamId(), cf); + } + if (debug.on()) + debug.log("Adding completed responseCF(0) with response headers"); + response_cfs.add(cf); + } finally { + response_cfs_lock.unlock(); + } + } + + @Override + void expectContinueFailed(int rcode) { + // Have to mark request as sent, due to no request body being sent in the + // event of a 417 Expectation Failed or some other non 100 response code + requestSent(); + } + + // methods to update state and remove stream when finished + + void requestSent() { + stateLock.lock(); + try { + requestSent0(); + } finally { + stateLock.unlock(); + } + } + + private void requestSent0() { + assert stateLock.isHeldByCurrentThread(); + requestSent = true; + if (responseReceived) { + if (debug.on()) debug.log("requestSent: streamid=%d", streamId()); + close(); + } else { + if (debug.on()) { + debug.log("requestSent: streamid=%d but response not received", streamId()); + } + } + } + + void responseReceived() { + stateLock.lock(); + try { + responseReceived0(); + } finally { + stateLock.unlock(); + } + } + + private void responseReceived0() { + assert stateLock.isHeldByCurrentThread(); + responseReceived = true; + if (requestSent) { + if (debug.on()) debug.log("responseReceived: streamid=%d", streamId()); + close(); + } else { + if (debug.on()) { + debug.log("responseReceived: streamid=%d but request not sent", streamId()); + } + } + } + + /** + * Same as {@link #completeResponse(Response)} above but for errors + */ + void completeResponseExceptionally(Throwable t) { + response_cfs_lock.lock(); + try { + // use index to avoid ConcurrentModificationException + // caused by removing the CF from within the loop. + for (int i = 0; i < response_cfs.size(); i++) { + CompletableFuture cf = response_cfs.get(i); + if (!cf.isDone()) { + response_cfs.remove(i); + cf.completeExceptionally(t); + return; + } + } + response_cfs.add(MinimalFuture.failedFuture(t)); + } finally { + response_cfs_lock.unlock(); + } + } + + @Override + void nullBody(HttpResponse resp, Throwable t) { + if (debug.on()) debug.log("nullBody: streamid=%d", streamId()); + // We should have an END_STREAM data frame waiting in the inputQ. + // We need a subscriber to force the scheduler to process it. + assert pendingResponseSubscriber == null; + pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null); + readScheduler.runOrSchedule(); + } + + /** + * An unprocessed exchange is one that hasn't been processed by a peer. The local end of the + * connection would be notified about such exchanges in either of the following 2 ways: + *

      + *
    • when it receives a GOAWAY frame with a stream id that tells which + * exchanges have been unprocessed. + *
    • or when a particular request's stream is reset with the H3_REQUEST_REJECTED error code. + *
    + *

    + * This method is called on such unprocessed exchanges and the implementation of this method + * will arrange for the request, corresponding to this exchange, to be retried afresh. + */ + void closeAsUnprocessed() { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed"; + // We arrange for the request to be retried on a new connection as allowed + // by RFC-9114, section 5.2 + this.exchange.markUnprocessedByPeer(); + this.errorRef.compareAndSet(null, new IOException("request not processed by peer")); + // close the exchange and complete the response CF exceptionally + close(); + completeResponseExceptionally(this.errorRef.get()); + if (debug.on()) { + debug.log("request unprocessed by peer " + this.request); + } + } + + // This method doesn't send any frame + void close() { + if (closed) return; + Throwable error; + stateLock.lock(); + try { + if (closed) return; + closed = true; + error = errorRef.get(); + } finally { + stateLock.unlock(); + } + if (Log.http3()) { + if (error == null) { + Log.logHttp3("Closed HTTP/3 exchange for {0}/streamId={1}", + connection.quicConnectionTag(), Long.toString(stream.streamId())); + } else { + Log.logHttp3("Closed HTTP/3 exchange for {0}/streamId={1} with error {2}", + connection.quicConnectionTag(), Long.toString(stream.streamId()), + error); + } + } + if (debug.on()) { + debug.log("stream %d is now closed with %s", + streamId(), + error == null ? "no error" : String.valueOf(error)); + } + if (Log.trace()) { + Log.logTrace("Stream {0} is now closed", streamId()); + } + + BodySubscriber subscriber = responseSubscriber; + if (subscriber == null) subscriber = pendingResponseSubscriber; + if (subscriber instanceof Http3StreamResponseSubscriber h3srs) { + // ensure subscriber is unregistered + h3srs.complete(error); + } + connection.onExchangeClose(this, streamId()); + } + + class RequestSubscriber implements Flow.Subscriber { + // can be < 0 if the actual length is not known. + private final long contentLength; + private volatile long remainingContentLength; + private volatile boolean dataHeaderWritten; + private volatile boolean completed; + private final AtomicReference subscription = new AtomicReference<>(); + + RequestSubscriber(long contentLen) { + this.contentLength = contentLen; + this.remainingContentLength = contentLen; + } + + @Override + public void onSubscribe(Subscription subscription) { + if (!this.subscription.compareAndSet(null, subscription)) { + subscription.cancel(); + throw new IllegalStateException("already subscribed"); + } + if (debug.on()) + debug.log("RequestSubscriber: onSubscribe, request 1"); + subscription.request(1); + } + + @Override + public void onNext(ByteBuffer item) { + if (debug.on()) + debug.log("RequestSubscriber: onNext(%d)", item.remaining()); + var subscription = this.subscription.get(); + if (writer.stopSendingReceived()) { + // whether StopSending contains NO_ERROR or not - we should + // not fail the request and simply stop sending the body. + // The sender should either reset the stream or send a full + // response with an error status code if it wants to fail the request. + Http3Error error = Http3Error.fromCode(writer.stream().sndErrorCode()) + .orElse(Http3Error.H3_NO_ERROR); + if (debug.on()) + debug.log("Stop sending requested by peer (%s): canceling subscription", error); + requestBodyCF.complete(null); + subscription.cancel(); + return; + } + + if (isCanceled() || errorRef.get() != null) { + if (writer.sendingState().isSending()) { + try { + if (debug.on()) { + debug.log("onNext called after stream cancelled: " + + "resetting stream %s", streamId()); + } + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: ", t); + errorRef.compareAndSet(null, t); + requestBodyCF.completeExceptionally(errorRef.get()); + } + } + return; + } + long len = item.remaining(); + try { + writeHeadersIfNeeded(item); + var remaining = remainingContentLength; + if (contentLength >= 0) { + remaining -= len; + remainingContentLength = remaining; + if (remaining < 0) { + lengthMismatch("Too many bytes in request body"); + subscription.cancel(); + } + } + var completed = remaining == 0; + if (completed) this.completed = true; + writer.scheduleForWriting(item, completed); + sentQuicBytes.addAndGet(len); + if (completed) { + requestBodyCF.complete(null); + } + if (writer.credit() > 0) { + if (debug.on()) + debug.log("RequestSubscriber: request 1"); + subscription.request(1); + } else { + if (debug.on()) + debug.log("RequestSubscriber: no more credit"); + } + } catch (Throwable t) { + if (writer.stopSendingReceived()) { + // We can reach here if we continue sending after stop sending + // was received, which may happen since stop sending is + // received asynchronously. In that case, we should + // not fail the request but simply stop sending the body. + // The sender will either reset the stream or send a full + // response with an error status code if it wants to fail + // or complete the request. + if (debug.on()) + debug.log("Stop sending requested by peer: canceling subscription"); + requestBodyCF.complete(null); + subscription.cancel(); + return; + } + // stop sending was not received: cancel the stream + errorRef.compareAndSet(null, t); + if (debug.on()) { + debug.log("Unexpected exception in onNext: " + t); + debug.log("resetting stream %s", streamId()); + } + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable rt) { + if (debug.on()) + debug.log("Failed to reset stream: %s", t); + } + cancelImpl(errorRef.get(), Http3Error.H3_REQUEST_CANCELLED); + } + + } + + private void lengthMismatch(String what) { + if (debug.on()) { + debug.log(what + " (%s/%s)", + contentLength - remainingContentLength, contentLength); + } + try { + var failed = new IOException("stream=" + streamId() + " " + + "[" + Thread.currentThread().getName() + "] " + + what + " (" + + (contentLength - remainingContentLength) + "/" + + contentLength + ")"); + errorRef.compareAndSet(null, failed); + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + requestBodyCF.completeExceptionally(errorRef.get()); + } catch (Throwable t) { + if (debug.on()) + debug.log("Failed to reset stream: %s", t); + } + close(); + } + + private void writeHeadersIfNeeded(ByteBuffer item) throws IOException { + long len = item.remaining(); + if (contentLength >= 0) { + if (!dataHeaderWritten) { + dataHeaderWritten = true; + len = contentLength; + } else { + // headers already written: nothing to do. + return; + } + } + DataFrame df = new DataFrame(len); + ByteBuffer headers = ByteBuffer.allocate(df.headersSize()); + df.writeHeaders(headers); + headers.flip(); + int sent = headers.remaining(); + writer.queueForWriting(headers); + if (sent != 0) sentQuicBytes.addAndGet(sent); + } + + @Override + public void onError(Throwable throwable) { + if (debug.on()) + debug.log(() -> "RequestSubscriber: onError: " + throwable); + // ensure that errors are handled within the flow. + if (errorRef.compareAndSet(null, throwable)) { + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: %s", t); + } + requestBodyCF.completeExceptionally(throwable); + // no need to cancel subscription + close(); + } + } + + @Override + public void onComplete() { + if (debug.on()) debug.log("RequestSubscriber: send request body completed"); + var completed = this.completed; + if (completed || errorRef.get() != null) return; + if (contentLength >= 0 && remainingContentLength != 0) { + if (remainingContentLength < 0) { + lengthMismatch("Too many bytes in request body"); + } else { + lengthMismatch("Too few bytes returned by the publisher"); + } + return; + } + this.completed = true; + try { + writer.scheduleForWriting(QuicStreamReader.EOF, true); + requestBodyCF.complete(null); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to complete stream: " + t, t); + requestBodyCF.completeExceptionally(t); + } + } + + void unblock() { + if (completed || errorRef.get() != null) { + return; + } + var subscription = this.subscription.get(); + try { + if (writer.credit() > 0) { + if (subscription != null) { + subscription.request(1); + } + } + } catch (Throwable throwable) { + if (debug.on()) + debug.log(() -> "RequestSubscriber: unblock: " + throwable); + // ensure that errors are handled within the flow. + if (errorRef.compareAndSet(null, throwable)) { + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: %s", t); + } + requestBodyCF.completeExceptionally(throwable); + cancelImpl(throwable, Http3Error.H3_REQUEST_CANCELLED); + subscription.cancel(); + } + } + } + + } + + @Override + Response newResponse(HttpHeaders responseHeaders, int responseCode) { + this.responseCode = responseCode; + return this.response = new Response( + request, exchange, responseHeaders, connection(), + responseCode, Version.HTTP_3); + } + + protected void handleResponse() { + handleResponse(responseHeadersBuilder, rspHeadersConsumer, readScheduler, debug); + } + + protected void handlePromise(PushHeadersConsumer consumer) throws IOException { + PushPromiseState state = consumer.state; + PushPromiseFrame ppf = state.frame(); + promiseMap.remove(ppf); + long pushId = ppf.getPushId(); + + HttpHeaders promiseHeaders = state.headersBuilder().build(); + consumer.reset(); + + if (debug.on()) { + debug.log("received promise headers: %s", + promiseHeaders); + } + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("PUSH_PROMISE HEADERS (pushId: ") + .append(pushId).append("):\n"); + Log.dumpHeaders(sb, " ", promiseHeaders); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + String method = promiseHeaders.firstValue(":method") + .orElseThrow(() -> new ProtocolException("no method in promise request")); + String path = promiseHeaders.firstValue(":path") + .orElseThrow(() -> new ProtocolException("no path in promise request")); + String authority = promiseHeaders.firstValue(":authority") + .orElseThrow(() -> new ProtocolException("no authority in promise request")); + if (Set.of("PUT", "DELETE", "OPTIONS", "TRACE").contains(method)) { + throw new ProtocolException("push method not allowed pushId=" + pushId); + } + long clen = promiseHeaders.firstValueAsLong("Content-Length").orElse(-1); + if (clen > 0) { + throw new ProtocolException("push headers contain non-zero Content-Length for pushId=" + pushId); + } + if (promiseHeaders.firstValue("Transfer-Encoding").isPresent()) { + throw new ProtocolException("push headers contain Transfer-Encoding for pushId=" + pushId); + } + + + // this will clear the response headers + // At this point the push promise stream may not be opened yet + if (connection.onPushPromiseFrame(this, pushId, promiseHeaders)) { + // the promise response will be handled from a child of this exchange + // once the push stream is open, we have nothing more to do here. + if (debug.on()) { + debug.log("handling push promise response for %s with request-response stream %s", + pushId, streamId()); + } + } else { + // the promise response is being handled by another exchange, just accept the id + if (debug.on()) { + debug.log("push promise response for %s is already handled by another stream", + pushId); + } + PushGroup pushGroup = exchange.getPushGroup(); + connection.whenPushAccepted(pushId).thenAccept((accepted) -> { + if (accepted) { + pushGroup.acceptPushPromiseId(connection.newPushId(pushId)); + } + }); + } + } + + private void cancelPushPromise(PushPromiseState state, IOException cause) { + // send CANCEL_PUSH frame here + long pushId = state.frame().getPushId(); + connection.pushCancelled(pushId, cause); + } + + @Override + void onPollException(QuicStreamReader reader, IOException io) { + if (Log.http3()) { + Log.logHttp3("{0}/streamId={1} {2} #{3} (requestSent={4}, responseReceived={5}, " + + "reader={6}, writer={7}, statusCode={8}, finalStream={9}, " + + "receivedQuicBytes={10}, sentQuicBytes={11}): {12}", + connection().quicConnection().logTag(), + String.valueOf(reader.stream().streamId()), request, String.valueOf(exchange.multi.id), + requestSent, responseReceived, reader.receivingState(), writer.sendingState(), + String.valueOf(responseCode), connection.isFinalStream(), String.valueOf(receivedQuicBytes()), + String.valueOf(sentQuicBytes.get()), io); + } + } + + void onReaderReset() { + long errorCode = stream.rcvErrorCode(); + String resetReason = Http3Error.stringForCode(errorCode); + Http3Error resetError = Http3Error.fromCode(errorCode) + .orElse(Http3Error.H3_REQUEST_CANCELLED); + if (debug.on()) { + debug.log("Stream %s reset by peer [%s]: ", streamId(), resetReason); + } + // if the error is H3_REQUEST_REJECTED then it implies + // the request wasn't processed and the client is allowed to reissue + // that request afresh + if (resetError == Http3Error.H3_REQUEST_REJECTED) { + closeAsUnprocessed(); + } else if (!requestSent || !responseReceived) { + cancelImpl(new IOException("Stream %s reset by peer: %s" + .formatted(streamId(), resetReason)), + resetError); + } + if (debug.on()) { + debug.log("stopping scheduler for stream %s", streamId()); + } + readScheduler.stop(); + } + + + + // Invoked when some data is received from the request-response + // Quic stream + private void processQuicData() { + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + // + try { + processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug); + } catch (Throwable t) { + if (debug.on()) + debug.log("processQuicData - Unexpected exception", t); + if (!requestSent) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } else if (!responseReceived) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } + } finally { + if (debug.on()) + debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof()); + } + } + + void connectionError(Throwable throwable, long errorCode, String errMsg) { + if (errorRef.compareAndSet(null, throwable)) { + var streamid = streamId(); + if (debug.on()) { + if (streamid == -1) { + debug.log("cancelling stream due to connection error", throwable); + } else { + debug.log("cancelling stream " + streamid + " due to connection error", throwable); + } + } + if (Log.trace()) { + if (streamid == -1) { + Log.logTrace("connection error: {0}", errMsg); + } else { + var format = "cancelling stream {0} due to connection error: {1}"; + Log.logTrace(format, streamid, errMsg); + } + } + } + connection.connectionError(this, throwable, errorCode, errMsg); + } + + record PushPromiseState(PushPromiseFrame frame, + HeaderFrameReader reader, + HttpHeadersBuilder headersBuilder, + DecodingCallback consumer) {} + final ConcurrentHashMap promiseMap = new ConcurrentHashMap<>(); + + private void ignorePushPromiseData(PushPromiseFrame ppf, List payload) { + boolean completed = ppf.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + buf.limit(buf.position()); + if (buf == QuicStreamReader.EOF) { + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached promise: " + ppf), + Http3Error.H3_FRAME_ERROR); + } + } + + private boolean ignorePushPromiseFrame(PushPromiseFrame ppf, List payload) + throws IOException { + long pushId = ppf.getPushId(); + long minPushId = connection.getMinPushId(); + if (exchange.pushGroup == null) { + IOException checkFailed = connection.checkMaxPushId(pushId); + if (checkFailed != null) { + // connection is closed + throw checkFailed; + } + if (!connection.acceptPromises()) { + // if no stream accept promises, we can ignore the data and + // cancel the promise right away. + if (debug.on()) { + debug.log("ignoring PushPromiseFrame (no promise handler): %s%n", ppf); + } + ignorePushPromiseData(ppf, payload); + if (pushId >= minPushId) { + connection.noPushHandlerFor(pushId); + } + return true; + } + } + if (pushId < minPushId) { + if (debug.on()) { + debug.log("ignoring PushPromiseFrame (pushId=%s < %s): %s%n", + pushId, minPushId, ppf); + } + ignorePushPromiseData(ppf, payload); + return true; + } + return false; + } + + void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) + throws IOException { + var state = promiseMap.get(ppf); + if (state == null) { + if (ignorePushPromiseFrame(ppf, payload)) return; + if (debug.on()) + debug.log("received PushPromiseFrame: " + ppf); + var checkFailed = connection.checkMaxPushId(ppf.getPushId()); + if (checkFailed != null) throw checkFailed; + var builder = new HttpHeadersBuilder(); + var consumer = new PushHeadersConsumer(); + var reader = qpackDecoder.newHeaderFrameReader(consumer); + state = new PushPromiseState(ppf, reader, builder, consumer); + consumer.setState(state); + promiseMap.put(ppf, state); + } + if (debug.on()) + debug.log("receive promise headers: buffer list: " + payload); + HeaderFrameReader headerFrameReader = state.reader(); + boolean completed = ppf.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + headerFrameReader); + if (buf == QuicStreamReader.EOF) { + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached promise: " + ppf), + Http3Error.H3_FRAME_ERROR); + } + } + + /** + * This method is called by the {@link Http3PushManager} in order to + * invoke the {@link Acceptor} that will accept the push + * promise. This method gets the acceptor, invokes its {@link + * Acceptor#accepted()} method, and if {@code true}, returns the + * {@code Acceptor}. + *

    + * If the push request is not accepted this method returns {@code null}. + * + * @apiNote + * This method is called upon reception of a {@link PushPromiseFrame}. + * The quic stream that will carry the body may not be available yet. + * + * @param pushId the pushId + * @param pushRequest the promised push request + * @return an {@link Acceptor} to get the body handler for the + * push request, or {@code null}. + */ + Acceptor acceptPushPromise(long pushId, HttpRequestImpl pushRequest) { + if (Log.requests()) { + Log.logRequest("PUSH_PROMISE: " + pushRequest.toString()); + } + PushGroup pushGroup = exchange.getPushGroup(); + if (pushGroup == null || exchange.multi.requestCancelled()) { + if (Log.trace()) { + Log.logTrace("Rejecting push promise pushId: " + pushId); + } + connection.pushCancelled(pushId, null); + return null; + } + + Acceptor acceptor = null; + boolean accepted = false; + try { + acceptor = pushGroup.acceptPushRequest(pushRequest, connection.newPushId(pushId)); + accepted = acceptor.accepted(); + } catch (Throwable t) { + if (debug.on()) + debug.log("PushPromiseHandler::applyPushPromise threw exception %s", + (Object)t); + } + if (!accepted) { + // cancel / reject + if (Log.trace()) { + Log.logTrace("No body subscriber for {0}: {1}", pushRequest, + "Push " + pushId + " cancelled by users handler"); + } + connection.pushCancelled(pushId, null); + return null; + } + + assert accepted && acceptor != null; + return acceptor; + } + + /** + * This method is called by the {@link Http3PushManager} once the {@link Acceptor#cf() + * responseCF} has been obtained from the acceptor. + * @param pushId the pushId + * @param responseCF the response completable future + */ + void onPushRequestAccepted(long pushId, CompletableFuture> responseCF) { + PushGroup pushGroup = getExchange().getPushGroup(); + assert pushGroup != null; + // setup housekeeping for when the push is received + // TODO: deal with ignoring of CF anti-pattern + CompletableFuture> cf = responseCF; + cf.whenComplete((HttpResponse resp, Throwable t) -> { + t = Utils.getCompletionCause(t); + if (Log.trace()) { + Log.logTrace("Push {0} completed for {1}{2}", pushId, resp, + ((t==null) ? "": " with exception " + t)); + } + if (t != null) { + if (debug.on()) { + debug.log("completing pushResponseCF for" + + ", pushId=" + pushId + " with: " + t); + } + pushGroup.pushError(t); + } else { + if (debug.on()) { + debug.log("completing pushResponseCF for" + + ", pushId=" + pushId + " with: " + resp); + } + } + pushGroup.pushCompleted(); + }); + } + + /** + * This method is called by the {@link Http3PushPromiseStream} when + * starting + * @param pushRequest the pushRequest + * @param pushStream the pushStream + */ + void onHttp3PushStreamStarted(HttpRequestImpl pushRequest, + Http3PushPromiseStream pushStream) { + PushGroup pushGroup = getExchange().getPushGroup(); + assert pushGroup != null; + assert pushStream != null; + connection.onPushPromiseStreamStarted(pushStream, pushStream.streamId()); + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial header frame are received + void receiveHeaders(HeadersFrame headers, List payload) { + if (debug.on()) + debug.log("receive headers: buffer list: " + payload); + boolean completed = headers.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + // if we have finished receiving the header frame, pause reading until + // the status code has been decoded + if (endOfHeaders) switchReadingPaused(true); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + headerFrameReader); + if (buf == QuicStreamReader.EOF) { + eof = true; + // we are at EOF - no need to pause reading + switchReadingPaused(false); + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached: " + headers), + Http3Error.H3_FRAME_ERROR); + } + } + + + // Invoked when data can be pushed to the quic stream; + // Headers may block the stream - but they will be buffered in the stream + // so should not cause this method to be called. + // We should reach here only when sending body bytes. + private void sendQuicData() { + // This method is invoked when the sending part of the + // stream is unblocked. + if (!requestBodyCF.isDone()) { + if (!exchange.multi.requestCancelled()) { + var requestSubscriber = this.requestSubscriber; + // the requestSubscriber will request more data from + // upstream if needed + if (requestSubscriber != null) requestSubscriber.unblock(); + } + } + } + + // pushes entire response body into response subscriber + // blocking when required by local or remote flow control + CompletableFuture receiveResponseBody(BodySubscriber bodySubscriber, Executor executor) { + // ensure that the body subscriber will be subscribed and onError() is + // invoked + pendingResponseSubscriber = bodySubscriber; + + // We want to allow the subscriber's getBody() method to block, so it + // can work with InputStreams. So, we offload execution. + responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber, + new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED)); + + if (isCanceled()) { + Throwable t = getCancelCause(); + responseBodyCF.completeExceptionally(t); + } + + readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error + + return responseBodyCF; + } + + void onSubscriptionError(Throwable t) { + errorRef.compareAndSet(null, t); + if (debug.on()) debug.log("Got subscription error: %s", (Object) t); + // This is the special case where the subscriber + // has requested an illegal number of items. + // In this case, the error doesn't come from + // upstream, but from downstream, and we need to + // handle the error without waiting for the inputQ + // to be exhausted. + stopRequested = true; + readScheduler.runOrSchedule(); + } + + // This loop is triggered to push response body data into + // the body subscriber. It is called from the processQuicData + // loop. However, we cannot call onNext() if we have no demands. + // So we're using a responseData queue to buffer incoming data. + void pushResponseData(ConcurrentLinkedQueue> responseData) { + if (debug.on()) debug.log("pushResponseData"); + HttpResponse.BodySubscriber subscriber = responseSubscriber; + boolean done = false; + try { + if (subscriber == null) { + subscriber = responseSubscriber = pendingResponseSubscriber; + if (subscriber == null) { + // can't process anything yet + return; + } else { + if (debug.on()) debug.log("subscribing user subscriber"); + subscriber.onSubscribe(userSubscription); + } + } + while (!responseData.isEmpty() && errorRef.get() == null) { + List data = responseData.peek(); + List dsts = Collections.unmodifiableList(data); + long size = Utils.remaining(dsts, Long.MAX_VALUE); + boolean finished = dsts.contains(QuicStreamReader.EOF); + if (size == 0 && finished) { + responseData.remove(); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onComplete"); + } + if (debug.on()) debug.log("pushResponseData: onComplete"); + done = true; + subscriber.onComplete(); + responseReceived(); + return; + } else if (userSubscription.tryDecrement()) { + responseData.remove(); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onNext {0}", size); + } + if (debug.on()) debug.log("pushResponseData: onNext(%d)", size); + subscriber.onNext(dsts); + } else { + if (stopRequested) break; + if (debug.on()) debug.log("no demand"); + return; + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + if (debug.on()) debug.log("pushResponseData: EOF"); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onComplete"); + } + if (debug.on()) debug.log("pushResponseData: onComplete"); + done = true; + subscriber.onComplete(); + responseReceived(); + return; + } + } catch (Throwable throwable) { + if (debug.on()) debug.log("pushResponseData: unexpected exception", throwable); + errorRef.compareAndSet(null, throwable); + } finally { + if (done) responseData.clear(); + } + + Throwable t = errorRef.get(); + if (t != null) { + try { + if (debug.on()) + debug.log("calling subscriber.onError: %s", (Object) t); + subscriber.onError(t); + } catch (Throwable x) { + Log.logError("Subscriber::onError threw exception: {0}", x); + } finally { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + responseData.clear(); + } + } + } + + // This method is called by Http2Connection::decrementStreamCount in order + // to make sure that the stream count is decremented only once for + // a given stream. + boolean deRegister() { + return DEREGISTERED.compareAndSet(this, false, true); + } + + private static final VarHandle DEREGISTERED; + static { + try { + DEREGISTERED = MethodHandles.lookup() + .findVarHandle(Http3ExchangeImpl.class, "deRegistered", boolean.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java new file mode 100644 index 00000000000..92d51b101f7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.Http3ClientImpl.ConnectionRecovery; +import jdk.internal.net.http.Http3ClientImpl.PendingConnection; +import jdk.internal.net.http.Http3ClientImpl.StreamLimitReached; +import jdk.internal.net.http.common.Log; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/** + * This class keeps track of pending HTTP/3 connections + * to avoid making two connections to the same server + * in parallel. Methods in this class are not atomic. + * Therefore, it is expected that they will be called + * while holding a lock in order to ensure atomicity. + */ +class Http3PendingConnections { + + private final Map pendingAdvertised = new ConcurrentHashMap<>(); + private final Map pendingUnadvertised = new ConcurrentHashMap<>(); + + Http3PendingConnections() {} + + + // Called when recovery is needed for a given connection, with + // the request that got the StreamLimitException + // Should be called while holding Http3ClientImpl.lock + void streamLimitReached(String key, Http3Connection connection) { + var altSvc = connection.connection().getSourceAltService().orElse(null); + var advertised = altSvc != null && altSvc.wasAdvertised(); + var queue = advertised ? pendingAdvertised : pendingUnadvertised; + queue.computeIfAbsent(key, k -> new StreamLimitReached(connection)); + } + + // Remove a ConnectionRecovery after the connection was established + // Should be called while holding Http3ClientImpl.lock + ConnectionRecovery removeCompleted(String connectionKey, Exchange origExchange, Http3Connection conn) { + var altSvc = Optional.ofNullable(conn) + .map(Http3Connection::connection) + .flatMap(HttpQuicConnection::getSourceAltService) + .orElse(null); + var discovery = Optional.ofNullable(origExchange) + .map(Exchange::request) + .map(HttpRequestImpl::http3Discovery) + .orElse(null); + var advertised = (altSvc != null && altSvc.wasAdvertised()) + || discovery == ALT_SVC; + var sameOrigin = (altSvc != null && altSvc.originHasSameAuthority()); + + ConnectionRecovery recovered = null; + if (advertised) { + recovered = pendingAdvertised.remove(connectionKey); + } + if (discovery == ALT_SVC || recovered != null) return recovered; + if (altSvc == null) { + // for instance, there was an exception, so we don't + // know if there was an altSvc because conn == null + recovered = pendingAdvertised.get(connectionKey); + if (recovered instanceof PendingConnection pending) { + if (pending.exchange() == origExchange) { + pendingAdvertised.remove(connectionKey, recovered); + return recovered; + } + } + } + recovered = pendingUnadvertised.get(connectionKey); + if (recovered instanceof PendingConnection pending) { + if (pending.exchange() == origExchange) { + pendingUnadvertised.remove(connectionKey, recovered); + return pending; + } + } + if (!sameOrigin && advertised) return null; + return pendingUnadvertised.remove(connectionKey); + } + + // Lookup a ConnectionRecovery for the given request with the + // given key. + // Should be called while holding Http3ClientImpl.lock + ConnectionRecovery lookupFor(String key, HttpRequestImpl request, HttpClientImpl client) { + + var discovery = request.http3Discovery(); + + // if ALT_SVC only look in advertised + if (discovery == ALT_SVC) { + return pendingAdvertised.get(key); + } + + // if HTTP_3_ONLY look first in pendingUnadvertised + var unadvertised = pendingUnadvertised.get(key); + if (discovery == HTTP_3_URI_ONLY && unadvertised != null) { + if (unadvertised instanceof PendingConnection) { + return unadvertised; + } + } + + // then look in advertised + var advertised = pendingAdvertised.get(key); + if (advertised instanceof PendingConnection pending) { + var altSvc = pending.altSvc(); + var sameOrigin = altSvc != null && altSvc.originHasSameAuthority(); + assert altSvc != null; // pending advertised should have altSvc + if (discovery == ANY || sameOrigin) return advertised; + } + + // if HTTP_3_ONLY, nothing found, stop here + assert discovery != HTTP_3_URI_ONLY || !(unadvertised instanceof PendingConnection); + if (discovery == HTTP_3_URI_ONLY) { + if (advertised != null && Log.http3()) { + Log.logHttp3("{0} cannot be used for {1}: return null", advertised, request); + } + assert !(unadvertised instanceof PendingConnection); + return unadvertised; + } + + // if ANY return advertised if found, otherwise unadvertised + if (advertised instanceof PendingConnection) return advertised; + if (unadvertised instanceof PendingConnection) { + if (client.client3().isEmpty()) { + return unadvertised; + } + // if ANY and we have an alt service that's eligible for the request + // and is not same origin as the request's URI authority, then don't + // return unadvertised and instead return advertised (which may be null) + final AltService altSvc = client.client3().get().lookupAltSvc(request).orElse(null); + if (altSvc != null && !altSvc.originHasSameAuthority()) { + return advertised; + } else { + return unadvertised; + } + } + if (advertised != null) return advertised; + return unadvertised; + } + + // Adds a pending connection for the given request with the given + // key and altSvc. + // Should be called while holding Http3ClientImpl.lock + PendingConnection addPending(String key, HttpRequestImpl request, AltService altSvc, Exchange exchange) { + var discovery = request.http3Discovery(); + var advertised = altSvc != null && altSvc.wasAdvertised(); + var sameOrigin = altSvc == null || altSvc.originHasSameAuthority(); + // if advertised and same origin, we don't use pendingUnadvertised + // but pendingAdvertised even if discovery is HTTP_3_URI_ONLY + // if we have an advertised altSvc with not same origin, we still + // want to attempt HTTP_3_URI_ONLY at origin, as an unadvertised + // connection. If advertised & same origin, we can use the advertised + // service instead and use pendingAdvertised, even for HTTP_3_URI_ONLY + if (discovery == HTTP_3_URI_ONLY && (!advertised || !sameOrigin)) { + PendingConnection pendingConnection = new PendingConnection(null, exchange); + var previous = pendingUnadvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = "previous unadvertised pending connection found!" + + " (originally created for %s #%s) while adding pending connection for %s" + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + assert discovery != HTTP_3_URI_ONLY || advertised && sameOrigin; + if (advertised) { + PendingConnection pendingConnection = new PendingConnection(altSvc, exchange); + var previous = pendingAdvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = "previous pending advertised connection found!" + + " (originally created for %s #%s) while adding pending connection for %s" + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + if (discovery == ANY) { + assert !advertised; + PendingConnection pendingConnection = new PendingConnection(null, exchange); + var previous = pendingUnadvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = ("previous unadvertised pending connection found for ANY!" + + " (originally created for %s #%s) while adding pending connection for %s") + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + // last case - if we reach here we're ALT_SVC but couldn't + // find an advertised alt service. + assert discovery == ALT_SVC; + return null; + } +} + diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java new file mode 100644 index 00000000000..b9cf4dbc0f1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java @@ -0,0 +1,811 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; + +import static jdk.internal.net.http.Http3ClientProperties.MAX_HTTP3_PUSH_STREAMS; + +/** + * Manages HTTP/3 push promises for an HTTP/3 connection. + *

    + * This class maintains a bounded collection of recent push promises, + * together with the current state of the promise: pending, processed, or + * cancelled. When a new {@link jdk.internal.net.http.http3.frames.PushPromiseFrame} + * is received, and entry is added in the map, and the state of the promise + * is updated as it goes. + * When the map is full, old entries (lowest pushId) are expunged from + * the map. No promise will be accepted if its pushId is lower than the + * lowest pushId in the map. + * + * @apiNote + * When a PushPromiseFrame is received, {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)} + * is called. This arranges for an entry to be added to the map, unless there's + * already one. Also, the first Http3ExchangeImpl for which this method is called + * for a given pushId gets to handle the PushPromise: its {@link + * java.net.http.HttpResponse.PushPromiseHandler} will be invoked to accept the promise + * and handle the body. + *

    + * When a new PushStream is opened, {@link #onPushPromiseStream(QuicReceiverStream, long)} + * is called. When both {@code onPushPromiseFrame} and {@code onPushPromiseStream} have + * been called for a given {@code pushId}, an {@link Http3PushPromiseStream} is created + * and started to receive the body. + *

    + * {@link Http3ExchangeImpl} that receive a push promise frame, but don't get to handle + * the body (because it's already been delegated to another stream) should call + * {@link #whenAccepted(long)} to figure out when it is safe to invoke {@link + * PushGroup#acceptPushPromiseId(PushId)}. + *

    + * {@link #cancelPushPromise(long, Throwable, CancelPushReason)} can be called to cancel + * a push promise. {@link #pushPromiseProcessed(long)} should be called when the body + * has been fully processed. + */ +final class Http3PushManager { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + + private final ReentrantLock promiseLock = new ReentrantLock(); + private final ConcurrentHashMap promises = new ConcurrentHashMap<>(); + private final CompletableFuture DENIED = MinimalFuture.completedFuture(Boolean.FALSE); + private final CompletableFuture ACCEPTED = MinimalFuture.completedFuture(Boolean.TRUE); + + private final AtomicLong maxPushId = new AtomicLong(); + private final AtomicLong maxPushReceived = new AtomicLong(); + private final AtomicLong minPushId = new AtomicLong(); + // the max history we keep in the promiseMap. We start expunging old + // entries from the map when the size of the map exceeds this value + private static final long MAX_PUSH_HISTORY_SIZE = (3*MAX_HTTP3_PUSH_STREAMS)/2; + // the maxPushId increments, we send on MAX_PUSH_ID frame + // with a maxPushId incremented by that amount. + // Ideally should be <= to MAX_PUSH_HISTORY_SIZE, to avoid + // filling up the history right after the first MAX_PUSH_ID + private static final long MAX_PUSH_ID_INCREMENTS = MAX_HTTP3_PUSH_STREAMS; + private final Http3Connection connection; + + // number of pending promises + private final AtomicInteger pendingPromises = new AtomicInteger(); + // push promises are considered blocked if we have failed to send + // the last MAX_PUSH_ID update due to pendingPromises + // count having reached MAX_HTTP3_PUSH_STREAMS + private volatile boolean pushPromisesBlocked; + + + Http3PushManager(Http3Connection connection) { + this.connection = connection; + } + + String dbgTag() { + return connection.dbgTag(); + } + + public void cancelAllPromises(IOException closeCause, Http3Error error) { + for (var promise : promises.entrySet()) { + var pushId = promise.getKey(); + var pp = promise.getValue(); + switch (pp) { + case ProcessedPushPromise ignored -> {} + case CancelledPushPromise ignored -> {} + case PendingPushPromise ppp -> { + cancelPendingPushPromise(ppp, closeCause); + } + } + } + } + + // Different actions needs to be carried out when cancelling a + // push promise, depending on the state of the promise and the + // cancellation reason. + enum CancelPushReason { + NO_HANDLER, // the exchange has no PushGroup + PUSH_CANCELLED, // the PromiseHandler cancelled the push, + // or an error occurred handling the promise + CANCEL_RECEIVED; // received CANCEL_PUSH from server + } + + /** + * A PushPromise can be a PendingPushPromise, until the push + * response is completely received, or a ProcessedPushPromise, + * which replace the PendingPushPromise after the response body + * has been delivered. If the PushPromise is cancelled before + * accepting it or receiving a body, CancelledPushPromise will + * be recorded and replace the PendingPushPromise. + */ + private sealed interface PushPromise + permits PendingPushPromise, ProcessedPushPromise, CancelledPushPromise { + } + + /** + * Represent a PushPromise whose body as already been delivered + */ + private record ProcessedPushPromise(PushId pushId, HttpHeaders promiseHeaders) + implements PushPromise { } + + /** + * Represent a PushPromise that has been cancelled. No body will be delivered. + */ + private record CancelledPushPromise(PushId pushId) implements PushPromise { } + + // difficult to say what will come first - the push promise, + // or the push stream? + // The first push promise frame received will register the + // exchange with this class - and trigger the parsing of + // the request/response when the stream is available. + // The other will trigger a simple call to register the + // push id. + // Probably we also need some timer to clean + // up the map if the stream doesn't manifest after a while. + // We maintain minPushID, where any frame + // containing a push id < to the min will be discarded, + // and any stream with a pushId < will also be discarded. + + /** + * Represents a PushPromise whose body has not been delivered + * yet. + * @param the type of the body + */ + private static final class PendingPushPromise implements PushPromise { + // called when the first push promise frame is received + PendingPushPromise(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) { + this.accepted = new MinimalFuture<>(); + this.exchange = Objects.requireNonNull(exchange); + this.promiseHeaders = Objects.requireNonNull(promiseHeaders); + this.pushId = pushId; + } + + // called when the push promise stream is opened + PendingPushPromise(QuicReceiverStream stream, long pushId) { + this.accepted = new MinimalFuture<>(); + this.stream = Objects.requireNonNull(stream); + this.pushId = pushId; + } + + // volatiles should not be required since we only modify/read + // those within a lock. Final fields should ensure safe publication + final long pushId; // the push id + QuicReceiverStream stream; // the quic promise stream + Http3ExchangeImpl exchange; // the exchange that will create the body subscriber + Http3PushPromiseStream promiseStream; // the HTTP/3 stream to process the quic stream + HttpHeaders promiseHeaders; // the push promise request headers + CompletableFuture> responseCF; + HttpRequestImpl pushReq; + BodyHandler handler; + final CompletableFuture accepted; // whether the push promise was accepted + + public long pushId() { return pushId; } + + public boolean ready() { + if (stream == null) return false; + if (exchange == null) return false; + if (promiseHeaders == null) return false; + if (!accepted.isDone()) return false; + if (responseCF == null) return false; + if (pushReq == null) return false; + if (handler == null) return false; + return true; + } + + @Override + public String toString() { + return "PendingPushPromise{" + + "pushId=" + pushId + + ", stream=" + stream + + ", exchange=" + dbgTag(exchange) + + ", promiseStream=" + dbgTag(promiseStream) + + ", promiseHeaders=" + promiseHeaders + + ", accepted=" + accepted + + '}'; + } + + String dbgTag(Http3ExchangeImpl exchange) { + return exchange == null ? null : exchange.dbgTag(); + } + + String dbgTag(Http3PushPromiseStream promiseStream) { + return promiseStream == null ? null : promiseStream.dbgTag(); + } + } + + /** + * {@return the maximum pushId that can be accepted from the peer} + * This corresponds to the pushId that has been included in the last + * MAX_PUSH_ID frame sent to the peer. A pushId greater than this + * value must be rejected, and cause the connection to close with + * error. + * + * @apiNote due to internal constraints it is possible that the + * MAX_PUSH_ID frame has not been sent yet, but the {@code Http3PushManager} + * will behave as if the peer had received that frame. + * + * @see Http3Connection#checkMaxPushId(long) + * @see #checkMaxPushId(long) + */ + long getMaxPushId() { + return maxPushId.get(); + } + + /** + * {@return the minimum pushId that can be accepted from the peer} + * Any pushId strictly less than this value must be ignored. + * + * @apiNote The minimum pushId represents the smallest pushId that + * was recorded in our history. For smaller pushId, no history has + * been kept, due to history size constraints. Any pushId strictly + * less than this value must be ignored. + */ + long getMinPushId() { + return minPushId.get(); + } + + /** + * Called when a new push promise stream is created by the peer, and + * the pushId has been read. + * @param pushStream the new push promise stream + * @param pushId the pushId + */ + void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) { + assert pushId >= 0; + if (!connection.acceptLargerPushPromise(pushStream, pushId)) return; + PendingPushPromise promise = addPushPromise(pushStream, pushId); + if (promise != null) { + assert promise.stream == pushStream; + // if stream is avoilable start parsing? + tryReceivePromise(promise); + } + } + + /** + * Checks whether a MAX_PUSH_ID frame needs to be sent, + * and send it. + * Called from {@link Http3Connection#checkSendMaxPushId()}. + */ + void checkSendMaxPushId() { + if (MAX_PUSH_ID_INCREMENTS <= 0) return; + long pendingCount = pendingPromises.get(); + long availableSlots = MAX_HTTP3_PUSH_STREAMS - pendingCount; + if (availableSlots <= 0) { + pushPromisesBlocked = true; + if (debug.on()) debug.log("Push promises blocked: availableSlots=%s", pendingCount); + return; + } + long maxPushIdSent = maxPushId.get(); + long maxPushIdReceived = maxPushReceived.get(); + long half = Math.max(1, MAX_PUSH_ID_INCREMENTS /2); + if (maxPushIdSent - maxPushIdReceived < half) { + // do not send a maxPushId that would consume more + // than our available slots + long increment = Math.min(availableSlots, MAX_PUSH_ID_INCREMENTS); + long update = maxPushIdSent + increment; + boolean updated = false; + try { + // let's update the counter before sending the frame, + // otherwise there's a chance we can receive a frame + // before updating the counter. + do { + if (maxPushId.compareAndSet(maxPushIdSent, update)) { + if (debug.on()) { + debug.log("MAX_PUSH_ID updated: %s (%s -> %s), increment %s, pending %s, availableSlots %s", + update, maxPushIdSent, update, increment, + promises.values().stream().filter(PendingPushPromise.class::isInstance) + .map(p -> (PendingPushPromise) p) + .map(PendingPushPromise::pushId).toList(), + availableSlots); + } + updated = true; + break; + } + maxPushIdSent = maxPushId.get(); + } while (maxPushIdSent < update); + if (updated) { + if (pushPromisesBlocked) { + if (debug.on()) debug.log("Push promises unblocked: maxPushIdSent=%s", update); + pushPromisesBlocked = false; + } + connection.sendMaxPushId(update); + } + } catch (IOException io) { + debug.log("Failed to send MAX_PUSH_ID(%s): %s", update, io); + } + } + } + + /** + * Called when a PushPromiseFrame has been decoded. + * + * @apiNote + * This method calls {@link Http3ExchangeImpl#acceptPushPromise(long, HttpRequestImpl)} + * and {@link Http3ExchangeImpl#onPushRequestAccepted(long, CompletableFuture)} + * for the first exchange that receives the {@link + * jdk.internal.net.http.http3.frames.PushPromiseFrame} + * + * @param exchange The HTTP/3 exchange that received the frame + * @param pushId The pushId contained in the frame + * @param promiseHeaders The push promise headers contained in the frame + * + * @return true if the exchange should take care of creating the HttpResponse body, + * false otherwise + * + * @see Http3Connection#onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders) + */ + boolean onPushPromiseFrame(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) + throws IOException { + if (!connection.acceptLargerPushPromise(null, pushId)) return false; + PendingPushPromise promise = addPushPromise(exchange, pushId, promiseHeaders); + if (promise == null) { + return false; + } + // A PendingPushPromise is returned only if there was no + // PushPromise present. If a PendingPushPromise is returned + // it should therefore have its exchange already set to the + // current exchange. + assert promise.exchange == exchange; + HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest( + exchange.getExchange().request(), promiseHeaders); + var acceptor = exchange.acceptPushPromise(pushId, pushReq); + if (acceptor == null) { + // nothing to do: the push should already have been cancelled. + return false; + } + @SuppressWarnings("unchecked") + var pppU = (PendingPushPromise) promise; + var responseCF = pppU.responseCF; + assert responseCF == null; + boolean cancelled = false; + promiseLock.lock(); + try { + promise.pushReq = pushReq; + pppU.responseCF = responseCF = acceptor.cf(); + // recheck to verify the push hasn't been cancelled already + var check = promises.get(pushId); + if (check instanceof CancelledPushPromise || check == null) { + cancelled = true; + } else { + assert promise == check; + pppU.handler = acceptor.bodyHandler(); + } + } finally { + promiseLock.unlock(); + } + if (!cancelled) { + exchange.onPushRequestAccepted(pushId, responseCF); + promise.accepted.complete(true); + // if stream is available start parsing? + tryReceivePromise(promise); + return true; + } else { + cancelPendingPushPromise(promise, null); + // should be a no-op - in theory it should already + // have been completed + promise.accepted.complete(false); + return false; + } + } + + /** + * {@return a completable future that will be completed when a pushId has been + * accepted by the exchange in charge of creating the response body} + * + * The completable future is complete with {@code true} if the pushId is + * accepted, and with {@code false} if the pushId was rejected or cancelled. + * + * This method is intended to be called when {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false, + * indicating that the push promise is being delegated to another request/response + * exchange. + * On completion of the future returned here, if the future is completed + * with {@code true}, the caller is expected to call {@link + * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link + * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}. + * + * @see Http3Connection#whenPushAccepted(long) + * @param pushId the pushId + */ + CompletableFuture whenAccepted(long pushId) { + var promise = promises.get(pushId); + if (promise instanceof PendingPushPromise pp) { + return pp.accepted; + } else if (promise instanceof ProcessedPushPromise) { + return ACCEPTED; + } else { // CancelledPushPromise or null + return DENIED; + } + } + + + /** + * Cancel a push promise. In case of concurrent requests receiving the + * same pushId, where one has a PushPromiseHandler and the other doesn't, + * we will cancel the push only if reason != CANCEL_RECEIVED, or no request + * stream has already accepted the push. + * + * @param pushId the promise pushId + * @param cause the cause (can be null) + * @param reason reason for cancelling + */ + void cancelPushPromise(long pushId, Throwable cause, CancelPushReason reason) { + boolean sendCancelPush = false; + PendingPushPromise pending = null; + if (cause != null) { + debug.log("PushPromise cancelled: pushId=" + pushId, cause); + } else { + debug.log("PushPromise cancelled: pushId=%s", pushId); + String msg = "cancelPushPromise(pushId="+pushId+")"; + debug.log(msg); + } + if (reason == CancelPushReason.CANCEL_RECEIVED) { + if (checkMaxPushId(pushId) != null) { + // pushId >= max connection will be closed + return; + } + } + promiseLock.lock(); + try { + var promise = promises.get(pushId); + long min = minPushId.get(); + if (promise == null) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + if (pushId >= min) { + var cancelled = new CancelledPushPromise(connection.newPushId(pushId)); + promises.put(pushId, cancelled); + sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED; + } + } else if (promise instanceof CancelledPushPromise) { + // nothing to do + } else if (promise instanceof ProcessedPushPromise) { + // nothing we can do? + } else if (promise instanceof PendingPushPromise ppp) { + // only cancel if never accepted, or force cancel requested + if (ppp.promiseStream == null || reason != CancelPushReason.NO_HANDLER) { + var cancelled = new CancelledPushPromise(connection.newPushId(pushId)); + promises.put(pushId, cancelled); + long pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + ppp.accepted.complete(false); // NO OP if already completed + pending = ppp; + // send cancel push; do not send if we received + // a CancelPushFrame from the peer + // also do not update MAX_PUSH_ID here - MAX_PUSH_ID will + // be updated when starting the next request/response exchange that accepts + // push promises. + sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED; + } + } + } finally { + promiseLock.unlock(); + } + if (sendCancelPush) { + connection.sendCancelPush(pushId, cause); + } + if (pending != null) { + cancelPendingPushPromise(pending, cause); + } + } + + private void cancelPendingPushPromise(PendingPushPromise ppp, Throwable cause) { + var ps = ppp.stream; + var http3 = ppp.promiseStream; + var responseCF = ppp.responseCF; + if (ps != null) { + ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + if (http3 != null || responseCF != null) { + IOException io; + if (cause == null) { + io = new IOException("Push promise cancelled: " + ppp.pushId); + } else { + io = Utils.toIOException(cause); + } + if (http3 != null) { + http3.cancel(io); + } else if (responseCF != null) { + responseCF.completeExceptionally(io); + } + } + } + + /** + * Called when a push promise response body has been successfully received. + * @param pushId the pushId + */ + void pushPromiseProcessed(long pushId) { + promiseLock.lock(); + try { + var promise = promises.get(pushId); + if (promise instanceof PendingPushPromise ppp) { + var processed = new ProcessedPushPromise(connection.newPushId(pushId), + ppp.promiseHeaders); + promises.put(pushId, processed); + var pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + // do not update MAX_PUSH_ID here - MAX_PUSH_ID will + // be updated when starting the next request/response exchange that accepts + // push promises. + } + } finally { + promiseLock.unlock(); + } + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + IOException checkMaxPushId(long pushId) { + return connection.checkMaxPushId(pushId); + } + + // Checks whether an Http3PushPromiseStream can be created now + private void tryReceivePromise(PendingPushPromise promise) { + debug.log("tryReceivePromise: " + promise); + promiseLock.lock(); + Http3PushPromiseStream http3PushPromiseStream = null; + IOException failed = null; + try { + if (promise.ready() && promise.promiseStream == null) { + promise.promiseStream = http3PushPromiseStream = + createPushExchange(promise); + } else { + debug.log("tryReceivePromise: Can't create Http3PushPromiseStream for pushId=%s yet", + promise.pushId); + } + } catch (IOException io) { + failed = io; + } finally { + promiseLock.unlock(); + } + if (failed != null) { + cancelPushPromise(promise.pushId, failed, CancelPushReason.PUSH_CANCELLED); + return; + } + if (http3PushPromiseStream != null) { + // HTTP/3 push promises are not ref-counted + // If we were to change that it could be necessary to + // temporarly increment ref-counting here, until the stream + // read loop effectively starts. + http3PushPromiseStream.start(); + } + } + + // try to create and start an Http3PushPromiseStream when all bits have + // been received + private Http3PushPromiseStream createPushExchange(PendingPushPromise promise) + throws IOException { + assert promise.ready() : "promise is not ready: " + promise; + Http3ExchangeImpl parent = promise.exchange; + HttpRequestImpl pushReq = promise.pushReq; + QuicReceiverStream quicStream = promise.stream; + Exchange pushExch = new Exchange<>(pushReq, parent.exchange.multi); + Http3PushPromiseStream pushStream = new Http3PushPromiseStream<>(pushExch, + parent.http3Connection(), this, + quicStream, promise.responseCF, promise.handler, parent, promise.pushId); + pushExch.exchImpl = pushStream; + return pushStream; + } + + // The first exchange that gets the PushPromise gets a PushPromise object, + // others get null + // TODO: ideally we should start a timer to cancel a push promise if + // the stream doesn't materialize after a while. + // Note that the callers can always start their own timeouts using + // the CompletableFutures we returned to them. + private PendingPushPromise addPushPromise(Http3ExchangeImpl exchange, + long pushId, + HttpHeaders promiseHeaders) { + PushPromise promise = promises.get(pushId); + boolean cancelStream = false; + if (promise == null) { + promiseLock.lock(); + try { + promise = promises.get(pushId); + if (promise == null) { + if (checkMaxPushId(pushId) == null) { + if (pushId >= minPushId.get()) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + var pp = new PendingPushPromise<>(exchange, pushId, promiseHeaders); + promises.put(pushId, pp); + long pendingCount = pendingPromises.incrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + return pp; + } else { + // pushId < minPushId + cancelStream = true; + } + } else return null; + } + } finally { + promiseLock.unlock(); + } + } + if (cancelStream) { + // we don't have the stream; + // the stream will be canceled if it comes later + // do not send push cancel frame (already cancelled, or abandoned) + return null; + } + if (promise instanceof PendingPushPromise ppp) { + var pe = ppp.exchange; + if (pe == null) { + promiseLock.lock(); + try { + if (ppp.exchange == null) { + assert ppp.promiseHeaders == null; + @SuppressWarnings("unchecked") + var pppU = (PendingPushPromise) ppp; + pppU.exchange = exchange; + pppU.promiseHeaders = promiseHeaders; + return pppU; + } + } finally { + promiseLock.unlock(); + } + } + var previousHeaders = ppp.promiseHeaders; + if (previousHeaders != null && !previousHeaders.equals(promiseHeaders)) { + connection.protocolError( + new ProtocolException("push headers do not match with previous promise for " + pushId)); + } + } else if (promise instanceof ProcessedPushPromise ppp) { + if (!ppp.promiseHeaders().equals(promiseHeaders)) { + connection.protocolError( + new ProtocolException("push headers do not match with previous promise for " + pushId)); + } + } else if (promise instanceof CancelledPushPromise) { + // already cancelled - nothing to do + } + return null; + } + + // TODO: the packet opening the push promise stream might reach us before + // the push promise headers are processed. We could start a timer + // here to cancel the push promise if the PushPromiseFrame doesn't materialize + // after a while. + private PendingPushPromise addPushPromise(QuicReceiverStream stream, long pushId) { + PushPromise promise = promises.get(pushId); + boolean cancelStream = false; + if (promise == null) { + promiseLock.lock(); + try { + promise = promises.get(pushId); + if (promise == null) { + if (checkMaxPushId(pushId) == null) { + if (pushId >= minPushId.get()) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + var pp = new PendingPushPromise(stream, pushId); + promises.put(pushId, pp); + long pendingCount = pendingPromises.incrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + return pp; + } else { + // pushId < minPushId + cancelStream = true; + } + } else return null; // maxPushId exceeded, connection closed + } + } finally { + promiseLock.unlock(); + } + } + if (cancelStream) { + // do not send push cancel frame (already cancelled, or abandoned) + stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return null; + } + if (promise instanceof PendingPushPromise ppp) { + var ps = ppp.stream; + if (ps == null) { + promiseLock.lock(); + try { + if ((ps = ppp.stream) == null) { + ps = ppp.stream = stream; + } + } finally { + promiseLock.unlock(); + } + } + if (ps == stream) { + @SuppressWarnings("unchecked") + var pp = ((PendingPushPromise) ppp); + return pp; + } else { + // Error! cancel stream... + var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId)); + connection.connectionError(io, Http3Error.H3_ID_ERROR); + } + } else if (promise instanceof ProcessedPushPromise) { + var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId)); + connection.connectionError(io, Http3Error.H3_ID_ERROR); + } else { + // already cancelled? + // Error! cancel stream... + // connection.sendCancelPush(pushId, null); + stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + return null; + } + + // We only keep MAX_PUSH_HISTORY_SIZE entries in the map. + // If the map has more than MAX_PUSH_HISTORY_SIZE entries, we start expunging + // pushIds starting at minPushId. This method makes room for at least + // on push promise in the map + private void checkExpungePromiseMap() { + assert promiseLock.isHeldByCurrentThread(); + while (promises.size() >= MAX_PUSH_HISTORY_SIZE) { + long min = minPushId.getAndIncrement(); + var pp = promises.remove(min); + if (pp instanceof PendingPushPromise ppp) { + var pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + var http3 = ppp.promiseStream; + IOException io = null; + if (http3 != null) { + http3.cancel(io = new IOException("PushPromise cancelled")); + } + if (io == null) { + io = new IOException("PushPromise cancelled"); + } + connection.sendCancelPush(ppp.pushId, io); + var ps = ppp.stream; + if (ps != null) { + ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java new file mode 100644 index 00000000000..27aaa75891c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java @@ -0,0 +1,746 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.net.ProtocolException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.Http3PushManager.CancelPushReason; +import jdk.internal.net.http.common.HttpBodySubscriberWrapper; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SubscriptionBase; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import static jdk.internal.net.http.http3.Http3Error.H3_FRAME_UNEXPECTED; + +/** + * This class represents an HTTP/3 PushPromise stream. + */ +final class Http3PushPromiseStream extends Http3Stream { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3Connection connection; + private final HttpHeadersBuilder respHeadersBuilder; + private final PushRespHeadersConsumer respHeadersConsumer; + private final HeaderFrameReader respHeaderFrameReader; + private final Decoder qpackDecoder; + private final AtomicReference errorRef; + private final CompletableFuture pushCF = new MinimalFuture<>(); + private final CompletableFuture> responseCF; + private final QuicReceiverStream stream; + private final QuicStreamReader reader; + private final Http3ExchangeImpl parent; + private final long pushId; + private final Http3PushManager pushManager; + private final BodyHandler pushHandler; + + private final FramesDecoder framesDecoder = + new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnPromiseStream); + private final SequentialScheduler readScheduler = + SequentialScheduler.lockingScheduler(this::processQuicData); + private final ReentrantLock stateLock = new ReentrantLock(); + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForPushPromiseStream(); + + final SubscriptionBase userSubscription = + new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError); + + volatile boolean closed; + volatile BodySubscriber pendingResponseSubscriber; + volatile BodySubscriber responseSubscriber; + volatile CompletableFuture responseBodyCF; + volatile boolean responseReceived; + volatile int responseCode; + volatile Response response; + volatile boolean stopRequested; + private String dbgTag = null; + + Http3PushPromiseStream(Exchange exchange, + final Http3Connection connection, + final Http3PushManager pushManager, + final QuicReceiverStream stream, + final CompletableFuture> responseCF, + final BodyHandler pushHandler, + Http3ExchangeImpl parent, + long pushId) { + super(exchange); + this.responseCF = responseCF; + this.pushHandler = pushHandler; + this.errorRef = new AtomicReference<>(); + this.pushId = pushId; + this.connection = connection; + this.pushManager = pushManager; + this.stream = stream; + this.parent = parent; + this.respHeadersBuilder = new HttpHeadersBuilder(); + this.respHeadersConsumer = new PushRespHeadersConsumer(); + this.qpackDecoder = connection.qpackDecoder(); + this.respHeaderFrameReader = qpackDecoder.newHeaderFrameReader(respHeadersConsumer); + this.reader = stream.connectReader(readScheduler); + debug.log("Http3PushPromiseStream created"); + } + + void start() { + exchange.exchImpl = this; + parent.onHttp3PushStreamStarted(exchange.request(), this); + this.reader.start(); + } + + long pushId() { + return pushId; + } + + String dbgTag() { + if (dbgTag != null) return dbgTag; + long streamId = streamId(); + String sid = streamId == -1 ? "?" : String.valueOf(streamId); + String ctag = connection == null ? null : connection.dbgTag(); + String tag = "Http3PushPromiseStream(" + ctag + ", streamId=" + sid + ", pushId="+ pushId + ")"; + if (streamId == -1) return tag; + return dbgTag = tag; + } + + @Override + long streamId() { + var stream = this.stream; + return stream == null ? -1 : stream.streamId(); + } + + private final class PushRespHeadersConsumer extends StreamHeadersConsumer { + + public PushRespHeadersConsumer() { + super(Context.RESPONSE); + } + + void resetDone() { + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + @Override + String headerFieldType() { + return "PUSH RESPONSE HEADER FIELD"; + } + + @Override + Decoder qpackDecoder() { + return qpackDecoder; + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed push response: " + super.formatMessage(message, header); + } + + + @Override + HeaderFrameReader headerFrameReader() { + return respHeaderFrameReader; + } + + @Override + HttpHeadersBuilder headersBuilder() { + return respHeadersBuilder; + } + + @Override + void headersCompleted() { + handleResponse(); + } + + @Override + public long streamId() { + return stream.streamId(); + } + } + + @Override + HttpQuicConnection connection() { + return connection.connection(); + } + + + // The Http3StreamResponseSubscriber is registered with the HttpClient + // to ensure that it gets completed if the SelectorManager aborts due + // to unexpected exceptions. + private void registerResponseSubscriber(Http3PushStreamResponseSubscriber subscriber) { + if (client().registerSubscriber(subscriber)) { + debug.log("Reference response body for h3 stream: " + streamId()); + client().h3StreamReference(); + } + } + + private void unregisterResponseSubscriber(Http3PushStreamResponseSubscriber subscriber) { + if (client().unregisterSubscriber(subscriber)) { + debug.log("Unreference response body for h3 stream: " + streamId()); + client().h3StreamUnreference(); + } + } + + final class Http3PushStreamResponseSubscriber extends HttpBodySubscriberWrapper { + Http3PushStreamResponseSubscriber(BodySubscriber subscriber) { + super(subscriber); + } + + @Override + protected void unregister() { + unregisterResponseSubscriber(this); + } + + @Override + protected void register() { + registerResponseSubscriber(this); + } + } + + Http3PushStreamResponseSubscriber createResponseSubscriber(BodyHandler handler, + ResponseInfo response) { + debug.log("Creating body subscriber"); + return new Http3PushStreamResponseSubscriber<>(handler.apply(response)); + } + + @Override + CompletableFuture ignoreBody() { + try { + debug.log("Ignoring body"); + reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return MinimalFuture.completedFuture(null); + } catch (Throwable e) { + Log.logTrace("Error requesting stop sending for stream {0}: {1}", + streamId(), e.toString()); + return MinimalFuture.failedFuture(e); + } + } + + @Override + void cancel() { + debug.log("cancel"); + var stream = this.stream; + if ((stream == null)) { + cancel(new IOException("Stream cancelled before streamid assigned")); + } else { + cancel(new IOException("Stream " + stream.streamId() + " cancelled")); + } + } + + @Override + void cancel(IOException cause) { + cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED); + } + + @Override + void onProtocolError(IOException cause) { + final long streamId = stream.streamId(); + if (debug.on()) { + debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage()); + } + Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause); + cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + @Override + void released() { + + } + + @Override + void completed() { + + } + + @Override + boolean isCanceled() { + return errorRef.get() != null; + } + + @Override + Throwable getCancelCause() { + return errorRef.get(); + } + + @Override + void cancelImpl(Throwable e, Http3Error error) { + try { + var streamid = streamId(); + if (errorRef.compareAndSet(null, e)) { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", e); + else debug.log("cancelling stream " + streamid + ":", e); + } + if (Log.trace()) { + if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e); + else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e); + } + } else { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", (Object) e); + else debug.log("cancelling stream %s: %s", streamid, e); + } + } + + var firstError = errorRef.get(); + completeResponseExceptionally(firstError); + if (responseBodyCF != null) { + responseBodyCF.completeExceptionally(firstError); + } + // will send a RST_STREAM frame + var stream = this.stream; + if (connection.isOpen()) { + if (stream != null) { + if (debug.on()) + debug.log("request stop sending"); + stream.requestStopSending(error.code()); + } + } + } catch (Throwable ex) { + debug.log("failed cancelling request: ", ex); + Log.logError(ex); + } finally { + close(); + } + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + var cf = pushCF; + if (executor != null && !cf.isDone()) { + // protect from executing later chain of CompletableFuture operations from SelectorManager thread + cf = cf.thenApplyAsync(r -> r, executor); + } + Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf); + if (debug.on()) debug.log("Response future is %s", cf); + return cf; + } + + void completeResponse(Response r) { + debug.log("Response: " + r); + Log.logResponse(r::toString); + pushCF.complete(r); // not strictly required for push API + // start reading the body using the obtained BodySubscriber + CompletableFuture start = new MinimalFuture<>(); + start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor())) + .whenComplete((T body, Throwable t) -> { + if (t != null) { + responseCF.completeExceptionally(t); + debug.log("Cancelling push promise %s (stream %s) due to: %s", pushId, streamId(), t); + pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED); + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } else { + HttpResponseImpl resp = + new HttpResponseImpl<>(r.request, r, null, body, getExchange()); + debug.log("Completing responseCF: " + resp); + pushManager.pushPromiseProcessed(pushId); + responseCF.complete(resp); + } + }); + start.completeAsync(() -> null, getExchange().executor()); + } + + // methods to update state and remove stream when finished + + void responseReceived() { + stateLock.lock(); + try { + responseReceived0(); + } finally { + stateLock.unlock(); + } + } + + private void responseReceived0() { + assert stateLock.isHeldByCurrentThread(); + responseReceived = true; + if (debug.on()) debug.log("responseReceived: streamid=%d", streamId()); + close(); + } + + /** + * same as above but for errors + */ + void completeResponseExceptionally(Throwable t) { + pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED); + responseCF.completeExceptionally(t); + } + + void nullBody(HttpResponse resp, Throwable t) { + if (debug.on()) debug.log("nullBody: streamid=%d", streamId()); + // We should have an END_STREAM data frame waiting in the inputQ. + // We need a subscriber to force the scheduler to process it. + assert pendingResponseSubscriber == null; + pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null); + readScheduler.runOrSchedule(); + } + + @Override + CompletableFuture> sendHeadersAsync() { + return MinimalFuture.completedFuture(this); + } + + @Override + CompletableFuture> sendBodyAsync() { + return MinimalFuture.completedFuture(this); + } + + CompletableFuture> responseCF() { + return responseCF; + } + + + BodyHandler getPushHandler() { + // ignored parameters to function can be used as BodyHandler + return this.pushHandler; + } + + @Override + CompletableFuture readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) { + try { + Log.logTrace("Reading body on stream {0}", streamId()); + debug.log("Getting BodySubscriber for: " + response); + Http3PushStreamResponseSubscriber bodySubscriber = + createResponseSubscriber(handler, new ResponseInfoImpl(response)); + CompletableFuture cf = receiveResponseBody(bodySubscriber, executor); + + PushGroup pg = parent.exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(e)); + } + var bodyCF = cf; + return bodyCF; + } catch (Throwable t) { + // may be thrown by handler.apply + // TODO: Is this the right error code? + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + PushGroup pg = parent.exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + pg.pushError(t); + } + return MinimalFuture.failedFuture(t); + } + } + + // This method doesn't send any frame + void close() { + if (closed) return; + stateLock.lock(); + try { + if (closed) return; + closed = true; + } finally { + stateLock.unlock(); + } + if (debug.on()) debug.log("stream %d is now closed", streamId()); + Log.logTrace("Stream {0} is now closed", streamId()); + + BodySubscriber subscriber = responseSubscriber; + if (subscriber == null) subscriber = pendingResponseSubscriber; + if (subscriber instanceof Http3PushStreamResponseSubscriber h3srs) { + // ensure subscriber is unregistered + h3srs.complete(errorRef.get()); + } + connection.onPushPromiseStreamClosed(this, streamId()); + } + + @Override + Response newResponse(HttpHeaders responseHeaders, int responseCode) { + return this.response = new Response( + exchange.request, exchange, responseHeaders, connection(), + responseCode, Version.HTTP_3); + } + + protected void handleResponse() { + handleResponse(respHeadersBuilder, respHeadersConsumer, readScheduler, debug); + } + + @Override + void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) throws IOException { + readScheduler.stop(); + connectionError(new ProtocolException("Unexpected PUSH_PROMISE on push response stream"), H3_FRAME_UNEXPECTED); + } + + @Override + void onPollException(QuicStreamReader reader, IOException io) { + if (Log.http3()) { + Log.logHttp3("{0}/streamId={1} pushId={2} #{3} (responseReceived={4}, " + + "reader={5}, statusCode={6}, finalStream={9}): {10}", + connection().quicConnection().logTag(), + String.valueOf(reader.stream().streamId()), pushId, String.valueOf(exchange.multi.id), + responseReceived, reader.receivingState(), + String.valueOf(responseCode), connection.isFinalStream(), io); + } + } + + @Override + void onReaderReset() { + long errorCode = stream.rcvErrorCode(); + String resetReason = Http3Error.stringForCode(errorCode); + Http3Error resetError = Http3Error.fromCode(errorCode) + .orElse(Http3Error.H3_REQUEST_CANCELLED); + if (!responseReceived) { + cancelImpl(new IOException("Stream %s reset by peer: %s" + .formatted(streamId(), resetReason)), + resetError); + } + if (debug.on()) { + debug.log("Stream %s reset by peer [%s]: Stopping scheduler", + streamId(), resetReason); + } + readScheduler.stop(); + } + + // Invoked when some data is received from the request-response + // Quic stream + private void processQuicData() { + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + try { + processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug); + } catch (Throwable t) { + debug.log("processQuicData - Unexpected exception", t); + if (!responseReceived) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } + } finally { + debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof()); + } + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial header frame are received + void receiveHeaders(HeadersFrame headers, List payload) + throws IOException { + debug.log("receive headers: buffer list: " + payload); + boolean completed = headers.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + // if we have finished receiving the header frame, pause reading until + // the status code has been decoded + if (endOfHeaders) switchReadingPaused(true); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + respHeaderFrameReader); + if (buf == QuicStreamReader.EOF) { + // we are at EOF - no need to pause reading + switchReadingPaused(false); + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached: " + headers), + Http3Error.H3_REQUEST_CANCELLED); + } + } + + void connectionError(Throwable throwable, long errorCode, String errMsg) { + if (errorRef.compareAndSet(null, throwable)) { + var streamid = streamId(); + if (debug.on()) { + if (streamid == -1) { + debug.log("cancelling stream due to connection error", throwable); + } else { + debug.log("cancelling stream " + streamid + + " due to connection error", throwable); + } + } + if (Log.trace()) { + if (streamid == -1) { + Log.logTrace( "connection error: {0}", errMsg); + } else { + var format = "cancelling stream {0} due to connection error: {1}"; + Log.logTrace(format, streamid, errMsg); + } + } + } + connection.connectionError(this, throwable, errorCode, errMsg); + } + + + // pushes entire response body into response subscriber + // blocking when required by local or remote flow control + CompletableFuture receiveResponseBody(BodySubscriber bodySubscriber, Executor executor) { + // We want to allow the subscriber's getBody() method to block so it + // can work with InputStreams. So, we offload execution. + responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber, + new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED)); + + if (isCanceled()) { + Throwable t = getCancelCause(); + responseBodyCF.completeExceptionally(t); + } + + // ensure that the body subscriber will be subsribed and onError() is + // invoked + pendingResponseSubscriber = bodySubscriber; + readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error + + return responseBodyCF; + } + + void onSubscriptionError(Throwable t) { + errorRef.compareAndSet(null, t); + if (debug.on()) debug.log("Got subscription error: %s", (Object) t); + // This is the special case where the subscriber + // has requested an illegal number of items. + // In this case, the error doesn't come from + // upstream, but from downstream, and we need to + // handle the error without waiting for the inputQ + // to be exhausted. + stopRequested = true; + readScheduler.runOrSchedule(); + } + + // This loop is triggered to push response body data into + // the body subscriber. + void pushResponseData(ConcurrentLinkedQueue> responseData) { + debug.log("pushResponseData"); + boolean onCompleteCalled = false; + BodySubscriber subscriber = responseSubscriber; + boolean done = false; + try { + if (subscriber == null) { + subscriber = responseSubscriber = pendingResponseSubscriber; + if (subscriber == null) { + // can't process anything yet + return; + } else { + if (debug.on()) debug.log("subscribing user subscriber"); + subscriber.onSubscribe(userSubscription); + } + } + while (!responseData.isEmpty()) { + List data = responseData.peek(); + List dsts = Collections.unmodifiableList(data); + long size = Utils.remaining(dsts, Long.MAX_VALUE); + boolean finished = dsts.contains(QuicStreamReader.EOF); + if (size == 0 && finished) { + responseData.remove(); + Log.logTrace("responseSubscriber.onComplete"); + if (debug.on()) debug.log("pushResponseData: onComplete"); + subscriber.onComplete(); + done = true; + onCompleteCalled = true; + responseReceived(); + return; + } else if (userSubscription.tryDecrement()) { + responseData.remove(); + Log.logTrace("responseSubscriber.onNext {0}", size); + if (debug.on()) debug.log("pushResponseData: onNext(%d)", size); + subscriber.onNext(dsts); + } else { + if (stopRequested) break; + debug.log("no demand"); + return; + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + debug.log("pushResponseData: EOF"); + if (!onCompleteCalled) { + Log.logTrace("responseSubscriber.onComplete"); + if (debug.on()) debug.log("pushResponseData: onComplete"); + subscriber.onComplete(); + done = true; + onCompleteCalled = true; + responseReceived(); + return; + } + } + } catch (Throwable throwable) { + debug.log("pushResponseData: unexpected exception", throwable); + errorRef.compareAndSet(null, throwable); + } finally { + if (done) responseData.clear(); + } + + Throwable t = errorRef.get(); + if (t != null) { + try { + if (!onCompleteCalled) { + if (debug.on()) + debug.log("calling subscriber.onError: %s", (Object) t); + subscriber.onError(t); + } else { + if (debug.on()) + debug.log("already completed: dropping error %s", (Object) t); + } + } catch (Throwable x) { + Log.logError("Subscriber::onError threw exception: {0}", t); + } finally { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + responseData.clear(); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java new file mode 100644 index 00000000000..cdac68b47f1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; +import static jdk.internal.net.http.RedirectFilter.HTTP_NOT_MODIFIED; + +/** + * A common super class for the HTTP/3 request/response stream ({@link Http3ExchangeImpl} + * and the HTTP/3 push promises stream ({@link Http3PushPromiseStream}. + * @param the expected type of the response body + */ +sealed abstract class Http3Stream extends ExchangeImpl permits Http3ExchangeImpl, Http3PushPromiseStream { + enum ResponseState { PERMIT_HEADER, PERMIT_TRAILER, PERMIT_NONE } + + // count of bytes read from the Quic stream. This is weakly consistent and + // used for debug only. Must not be updated outside of processQuicData + private volatile long receivedQuicBytes; + // keep track of which HTTP/3 frames have been parsed and whether more header + // frames are permitted + private ResponseState responseState = ResponseState.PERMIT_HEADER; + // value of content-length header in the response header, or null + private Long contentLength; + // number of data bytes delivered to user subscriber + private long consumedDataBytes; + // switched to true if reading from the quic stream should be temporarily + // paused. After switching back to false, readScheduler.runOrSchedule() should + // called. + private volatile boolean readingPaused; + + // A temporary buffer for response body bytes + final ConcurrentLinkedQueue> responseData = new ConcurrentLinkedQueue<>(); + + private final AtomicInteger nonFinalResponseCount = new AtomicInteger(); + + + Http3Stream(Exchange exchange) { + super(exchange); + } + + /** + * Cancel the stream exchange on error + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param error an HTTP/3 error + */ + abstract void cancelImpl(Throwable throwable, Http3Error error); + + /** + * {@return the Quic stream id for this exchange (request/response or push response)} + */ + abstract long streamId(); + + /** + * A base class implementing {@link DecodingCallback} used for receiving + * and building HttpHeaders. Can be used for request headers, response headers, + * push response headers, or trailers. + */ + abstract class StreamHeadersConsumer extends ValidatingHeadersConsumer + implements DecodingCallback { + + private volatile boolean hasError; + + StreamHeadersConsumer(Context context) { + super(context); + } + + abstract Decoder qpackDecoder(); + + abstract HeaderFrameReader headerFrameReader(); + + abstract HttpHeadersBuilder headersBuilder(); + + abstract void resetDone(); + + @Override + public void reset() { + super.reset(); + headerFrameReader().reset(); + headersBuilder().clear(); + hasError = false; + resetDone(); + } + + String headerFieldType() {return "HEADER FIELD";} + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + try { + String n = name.toString(); + String v = value.toString(); + super.onDecoded(n, v); + headersBuilder().addHeader(n, v); + if (Log.headers() && Log.trace()) { + Log.logTrace("RECEIVED {0} (streamid={1}): {2}: {3}", + headerFieldType(), streamId(), n, v); + } + } catch (Throwable throwable) { + if (throwable instanceof UncheckedIOException uio) { + // UncheckedIOException is thrown by ValidatingHeadersConsumer.onDecoded + // for cases with invalid headers or unknown/unsupported pseudo-headers. + // It should be treated as a malformed request. + // RFC-9114 4.1.2. Malformed Requests and Responses: + // Malformed requests or responses that are + // detected MUST be treated as a stream error of + // type H3_MESSAGE_ERROR. + onStreamError(uio.getCause(), Http3Error.H3_MESSAGE_ERROR); + } else { + onConnectionError(throwable, Http3Error.H3_INTERNAL_ERROR); + } + } + } + + @Override + public void onComplete() { + // RFC-9204 2.2.2.1: After the decoder finishes decoding a field + // section encoded using representations containing dynamic table + // references, it MUST emit a Section Acknowledgment instruction + qpackDecoder().ackSection(streamId(), headerFrameReader()); + qpackDecoder().resetInsertionsCounter(); + headersCompleted(); + } + + abstract void headersCompleted(); + + @Override + public void onStreamError(Throwable throwable, Http3Error http3Error) { + hasError = true; + qpackDecoder().resetInsertionsCounter(); + // Stream error + cancelImpl(throwable, http3Error); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + hasError = true; + // Connection error + connectionError(throwable, http3Error); + } + + @Override + public boolean hasError() { + return hasError; + } + + } + + /** + * {@return count of bytes read from the QUIC stream so far} + */ + public long receivedQuicBytes() { + return receivedQuicBytes; + } + + /** + * Notify of a connection error. + * + * The implementation of this method is supposed to close all + * exchanges, cancel all push promises, and close the connection. + * + * @implSpec + * The implementation of this method calls + * {@snippet lang=java : + * connectionError(throwable, error.code(), throwable.getMessage()); + * } + * + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param error an HTTP/3 error + */ + void connectionError(Throwable throwable, Http3Error error) { + connectionError(throwable, error.code(), throwable.getMessage()); + } + + + /** + * Notify of a connection error. + * + * The implementation of this method is supposed to close all + * exchanges, cancel all push promises, and close the connection. + * + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param errorCode an HTTP/3 error code + * @param errMsg an error message to be logged when closing the connection + */ + abstract void connectionError(Throwable throwable, long errorCode, String errMsg); + + + /** + * Push response data to the {@linkplain java.net.http.HttpResponse.BodySubscriber + * response body subscriber} if allowed by the subscription state. + * @param responseData a queue of available data to be pushed to the subscriber + */ + abstract void pushResponseData(ConcurrentLinkedQueue> responseData); + + /** + * Called when an exception is thrown by {@link QuicStreamReader#poll() reader::poll} + * when called from {@link #processQuicData(QuicStreamReader, FramesDecoder, + * H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData}. + * This is typically only used for logging purposes. + * @param reader the stream reader + * @param io the exception caught + */ + abstract void onPollException(QuicStreamReader reader, IOException io); + + /** + * Called when new payload data is received by {@link #processQuicData(QuicStreamReader, + * FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData} + * for a given header frame. + *

    + * Any exception thrown here will be rethrown by {@code processQuicData} + * + * @param headers a partially received header frame + * @param payload the payload bytes available for that frame + * @throws IOException if an error is detected + */ + abstract void receiveHeaders(HeadersFrame headers, List payload) throws IOException; + + /** + * Called when new payload data is received by {@link #processQuicData(QuicStreamReader, + * FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData} + * for a given push promise frame. + *

    + * Any exception thrown here will be rethrown by {@code processQuicData} + * + * @param ppf a partially received push promise frame + * @param payload the payload bytes available for that frame + * @throws IOException if an error is detected + */ + abstract void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) throws IOException; + + /** + * {@return whether reading from the quic stream is currently paused} + * Typically reading is paused when waiting for headers to be decoded by QPack. + */ + boolean readingPaused() {return readingPaused;} + + /** + * Switches the value of the {@link #readingPaused() readingPaused} + * flag + *

    + * Subclasses of {@code Http3Stream} can call this method to switch + * the value of this flag if needed, typically in their + * concrete implementation of {@link #receiveHeaders(HeadersFrame, List)}. + * @param value the new value + */ + void switchReadingPaused(boolean value) { + readingPaused = value; + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial data frame are received. + private void receiveData(DataFrame data, List payload, Logger debug) { + if (debug.on()) { + debug.log("receiveData: adding %s payload byte", Utils.remaining(payload)); + } + responseData.add(payload); + pushResponseData(responseData); + } + + private ByteBuffer pollIfNotReset(QuicStreamReader reader) throws IOException { + ByteBuffer buffer; + try { + if (reader.isReset()) return null; + buffer = reader.poll(); + } catch (IOException io) { + if (reader.isReset()) return null; + onPollException(reader, io); + throw io; + } + return buffer; + } + + private Throwable toThrowable(MalformedFrame malformedFrame) { + Throwable cause = malformedFrame.getCause(); + if (cause != null) return cause; + return new ProtocolException(malformedFrame.toString()); + } + + /** + * Called when {@code processQuicData} detects that the {@linkplain + * QuicStreamReader reader} has been reset. + * This method should do the appropriate garbage collection, + * possibly closing the exchange or the connection if needed, and + * closing the read scheduler. + */ + abstract void onReaderReset(); + + /** + * Invoked when some data is received from the underlying quic stream. + * This implements the read loop for a request-response stream or a + * push response stream. + */ + void processQuicData(QuicStreamReader reader, + FramesDecoder framesDecoder, + H3FrameOrderVerifier frameOrderVerifier, + SequentialScheduler readScheduler, + Logger debug) throws IOException { + + + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + // + + // the reader might be null if the loop is triggered before + // the field is assigned + if (reader == null) return; + + // check whether we need to wait until response headers + // have been decoded: in that case readingPaused will be true + if (readingPaused) return; + + if (debug.on()) debug.log("processQuicData"); + ByteBuffer buffer; + Http3Frame frame; + pushResponseData(responseData); + boolean readmore = responseData.isEmpty(); + // do not read more until data has been pulled + while (readmore && (buffer = pollIfNotReset(reader)) != null) { + if (debug.on()) + debug.log("processQuicData - submitting buffer: %s bytes (ByteBuffer@%s)", + buffer.remaining(), System.identityHashCode(buffer)); + // only updated here + var received = receivedQuicBytes; + receivedQuicBytes = received + buffer.remaining(); + framesDecoder.submit(buffer); + while ((frame = framesDecoder.poll()) != null) { + if (debug.on()) debug.log("processQuicData - frame: " + frame); + final long frameType = frame.type(); + // before we start processing, verify that this frame *type* has arrived in the + // allowed order + if (!frameOrderVerifier.allowsProcessing(frame)) { + final String unexpectedFrameType = Http3FrameType.asString(frameType); + // not expected to be arriving now + // RFC-9114, section 4.1 - Receipt of an invalid sequence of frames MUST be + // treated as a connection error of type H3_FRAME_UNEXPECTED. + if (debug.on()) { + debug.log("unexpected (order of) frame type: " + + unexpectedFrameType + " on stream"); + } + Log.logError("Connection error due to unexpected (order of) frame type" + + " {0} on stream", unexpectedFrameType); + readScheduler.stop(); + final String errMsg = "Unexpected frame " + unexpectedFrameType; + connectionError(new ProtocolException(errMsg), Http3Error.H3_FRAME_UNEXPECTED); + return; + } + if (frame instanceof PartialFrame partialFrame) { + final List payload = framesDecoder.readPayloadBytes(); + if (debug.on()) { + debug.log("processQuicData - payload: %s", + payload == null ? null : Utils.remaining(payload)); + } + if (framesDecoder.eof() && !framesDecoder.clean()) { + String msg = "Frame truncated: " + partialFrame; + connectionError(new ProtocolException(msg), + Http3Error.H3_FRAME_ERROR.code(), + msg); + break; + } + if ((payload == null || payload.isEmpty()) && partialFrame.remaining() != 0) { + break; + } + if (partialFrame instanceof HeadersFrame headers) { + receiveHeaders(headers, payload); + // check if we need to wait for the status code to be decoded + // before reading more + readmore = !readingPaused; + } else if (partialFrame instanceof DataFrame data) { + if (responseState != ResponseState.PERMIT_TRAILER) { + cancelImpl(new IOException("DATA frame not expected here"), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (payload != null) { + consumedDataBytes += Utils.remaining(payload); + if (contentLength != null && + consumedDataBytes + data.remaining() > contentLength) { + cancelImpl(new IOException( + String.format("DATA frame (length %d) exceeds content-length (%d) by %d", + data.streamingLength(), contentLength, + consumedDataBytes + data.remaining() - contentLength)), + Http3Error.H3_MESSAGE_ERROR); + return; + } + // don't read more if there is pending data waiting + // to be read from downstream + readmore = responseData.isEmpty(); + receiveData(data, payload, debug); + } + } else if (partialFrame instanceof PushPromiseFrame ppf) { + receivePushPromiseFrame(ppf, payload); + } else if (partialFrame instanceof UnknownFrame) { + if (debug.on()) { + debug.log("ignoring %s bytes for unknown frame type: %s", + Utils.remaining(payload), + Http3FrameType.asString(frameType)); + } + } else { + // should never come here: the only frame that we can + // receive on a request-response stream are + // HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN + // All have already been taken care above. + // So this here should be dead-code. + String msg = "unhandled frame type: " + + Http3FrameType.asString(frameType); + if (debug.on()) debug.log("Warning: %s", msg); + throw new AssertionError(msg); + } + // mark as complete, if all expected data has been read for a frame + if (partialFrame.remaining() == 0) { + frameOrderVerifier.completed(frame); + } + } else if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + readScheduler.stop(); + connectionError(toThrowable(malformed), + malformed.getErrorCode(), + malformed.getMessage()); + return; + } else { + // should never come here: the only frame that we can + // receive on a request-response stream are + // HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN + // All should have already been taken care above, + // including malformed frames. So this here should be + // dead-code. + String msg = "unhandled frame type: " + + Http3FrameType.asString(frameType); + if (debug.on()) debug.log("Warning: %s", msg); + throw new AssertionError(msg); + } + if (framesDecoder.eof()) break; + } + if (framesDecoder.eof()) break; + } + if (framesDecoder.eof()) { + if (!framesDecoder.clean()) { + String msg = "EOF reading frame type and length"; + connectionError(new ProtocolException(msg), + Http3Error.H3_FRAME_ERROR.code(), + msg); + } + if (debug.on()) debug.log("processQuicData - EOF"); + if (responseState == ResponseState.PERMIT_HEADER) { + cancelImpl(new EOFException("EOF reached: no header bytes received"), Http3Error.H3_MESSAGE_ERROR); + } else { + if (contentLength != null && + consumedDataBytes != contentLength) { + cancelImpl(new IOException( + String.format("fixed content-length: %d, bytes received: %d", contentLength, consumedDataBytes)), + Http3Error.H3_MESSAGE_ERROR); + return; + } + receiveData(new DataFrame(0), + List.of(QuicStreamReader.EOF), debug); + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + if (debug.on()) debug.log("EOF: Stopping scheduler"); + readScheduler.stop(); + } + if (reader.isReset() && responseData.isEmpty()) { + onReaderReset(); + } + } + + final String checkInterimResponseCountExceeded() { + // this is also checked by Exchange - but tracking it here too provides + // a more informative message. + int count = nonFinalResponseCount.incrementAndGet(); + if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) { + return String.format( + "Stream %s PROTOCOL_ERROR: too many interim responses received: %s > %s", + streamId(), count, MAX_NON_FINAL_RESPONSES); + } + return null; + } + + /** + * Called to create a new Response object for the newly receive response headers and + * response status code. This method is called from {@link #handleResponse(HttpHeadersBuilder, + * StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, after the status code + * and headers have been validated. + * + * @param responseHeaders response headers + * @param responseCode response code + * @return a new {@code Response} object + */ + abstract Response newResponse(HttpHeaders responseHeaders, int responseCode); + + /** + * Called at the end of {@link #handleResponse(HttpHeadersBuilder, + * StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, to propagate + * the response to the multi exchange. + * @param response the {@code Response} that was received. + */ + abstract void completeResponse(Response response); + + /** + * Validate response headers and status code based on the {@link #responseState}. + * If validated, this method will call {@link #newResponse(HttpHeaders, int)} to + * create a {@code Response} object, which it will then pass to + * {@link #completeResponse(Response)}. + * + * @param responseHeadersBuilder the response headers builder + * @param rspHeadersConsumer the response headers consumer + * @param readScheduler the read scheduler + * @param debug the debug logger + */ + void handleResponse(HttpHeadersBuilder responseHeadersBuilder, + StreamHeadersConsumer rspHeadersConsumer, + SequentialScheduler readScheduler, + Logger debug) { + if (responseState == ResponseState.PERMIT_NONE) { + connectionError(new ProtocolException("HEADERS after trailer"), + Http3Error.H3_FRAME_UNEXPECTED.code(), + "HEADERS after trailer"); + return; + } + HttpHeaders responseHeaders = responseHeadersBuilder.build(); + if (responseState == ResponseState.PERMIT_TRAILER) { + if (responseHeaders.firstValue(":status").isPresent()) { + cancelImpl(new IOException("Unexpected :status header in trailer"), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (Log.headers()) { + Log.logHeaders("Ignoring trailers on stream {0}: {1}", streamId(), responseHeaders); + } else if (debug.on()) { + debug.log("Ignoring trailers: %s", responseHeaders); + } + responseState = ResponseState.PERMIT_NONE; + rspHeadersConsumer.reset(); + if (readingPaused) { + readingPaused = false; + readScheduler.runOrSchedule(exchange.executor()); + } + return; + } + + int responseCode; + boolean finalResponse = false; + try { + responseCode = (int) responseHeaders + .firstValueAsLong(":status") + .orElseThrow(() -> new IOException("no statuscode in response")); + } catch (IOException | NumberFormatException exception) { + // RFC-9114: 4.1.2. Malformed Requests and Responses: + // "Malformed requests or responses that are + // detected MUST be treated as a stream error of type H3_MESSAGE_ERROR" + cancelImpl(exception, Http3Error.H3_MESSAGE_ERROR); + return; + } + if (responseCode < 100 || responseCode > 999) { + cancelImpl(new IOException("Unexpected :status header value"), Http3Error.H3_MESSAGE_ERROR); + return; + } + + if (responseCode >= 200) { + responseState = ResponseState.PERMIT_TRAILER; + finalResponse = true; + } else { + assert responseCode >= 100 && responseCode <= 200 : "unexpected responseCode: " + responseCode; + String protocolErrorMsg = checkInterimResponseCountExceeded(); + if (protocolErrorMsg != null) { + if (debug.on()) { + debug.log(protocolErrorMsg); + } + cancelImpl(new ProtocolException(protocolErrorMsg), Http3Error.H3_GENERAL_PROTOCOL_ERROR); + rspHeadersConsumer.reset(); + return; + } + } + + // update readingPaused after having decoded the statusCode and + // switched the responseState. + if (readingPaused) { + readingPaused = false; + readScheduler.runOrSchedule(exchange.executor()); + } + + var response = newResponse(responseHeaders, responseCode); + + if (debug.on()) { + debug.log("received response headers: %s", + responseHeaders); + } + + try { + OptionalLong cl = responseHeaders.firstValueAsLong("content-length"); + if (finalResponse && cl.isPresent()) { + long cll = cl.getAsLong(); + if (cll < 0) { + cancelImpl(new IOException("Invalid content-length value "+cll), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (!(exchange.request().method().equalsIgnoreCase("HEAD") || responseCode == HTTP_NOT_MODIFIED)) { + // HEAD response and 304 response might have a content-length header, + // but it carries no meaning + contentLength = cll; + } + } + } catch (NumberFormatException nfe) { + cancelImpl(nfe, Http3Error.H3_MESSAGE_ERROR); + return; + } + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("H3 RESPONSE HEADERS (stream="); + sb.append(streamId()).append(")\n"); + Log.dumpHeaders(sb, " ", responseHeaders); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + // this will clear the response headers + rspHeadersConsumer.reset(); + + completeResponse(response); + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java index c58f0b0c752..b73b92add63 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java @@ -41,6 +41,7 @@ import java.net.ProtocolException; import java.net.ProxySelector; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpTimeoutException; +import java.net.http.UnsupportedProtocolVersionException; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; @@ -93,8 +94,16 @@ import jdk.internal.net.http.common.TimeSource; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.common.OperationTrackers.Trackable; import jdk.internal.net.http.common.OperationTrackers.Tracker; +import jdk.internal.net.http.common.Utils.SafeExecutor; +import jdk.internal.net.http.common.Utils.SafeExecutorService; import jdk.internal.net.http.websocket.BuilderImpl; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.util.Objects.requireNonNullElse; +import static java.util.Objects.requireNonNullElseGet; +import static jdk.internal.net.quic.QuicTLSContext.isQuicCompatible; + /** * Client implementation. Contains all configuration information and also * the selector manager thread which allows async events to be registered @@ -112,7 +121,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final int DEFAULT_KEEP_ALIVE_TIMEOUT = 30; static final long KEEP_ALIVE_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout", DEFAULT_KEEP_ALIVE_TIMEOUT); // Defaults to value used for HTTP/1 Keep-Alive Timeout. Can be overridden by jdk.httpclient.keepalive.timeout.h2 property. - static final long IDLE_CONNECTION_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); + static final long IDLE_CONNECTION_TIMEOUT_H2 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); + static final long IDLE_CONNECTION_TIMEOUT_H3 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h3", IDLE_CONNECTION_TIMEOUT_H2); // Define the default factory as a static inner class // that embeds all the necessary logic to avoid @@ -145,15 +155,23 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final class DelegatingExecutor implements Executor { private final BooleanSupplier isInSelectorThread; private final Executor delegate; + private final SafeExecutor safeDelegate; private final BiConsumer errorHandler; DelegatingExecutor(BooleanSupplier isInSelectorThread, Executor delegate, BiConsumer errorHandler) { this.isInSelectorThread = isInSelectorThread; this.delegate = delegate; + this.safeDelegate = delegate instanceof ExecutorService svc + ? new SafeExecutorService(svc, ASYNC_POOL, errorHandler) + : new SafeExecutor<>(delegate, ASYNC_POOL, errorHandler); this.errorHandler = errorHandler; } + Executor safeDelegate() { + return safeDelegate; + } + Executor delegate() { return delegate; } @@ -325,6 +343,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { private final SelectorManager selmgr; private final FilterFactory filters; private final Http2ClientImpl client2; + private final Http3ClientImpl client3; + private final AltServicesRegistry registry; private final long id; private final String dbgTag; private final InetAddress localAddr; @@ -386,6 +406,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { private final AtomicLong pendingHttpOperationsCount = new AtomicLong(); private final AtomicLong pendingHttpRequestCount = new AtomicLong(); private final AtomicLong pendingHttp2StreamCount = new AtomicLong(); + private final AtomicLong pendingHttp3StreamCount = new AtomicLong(); private final AtomicLong pendingTCPConnectionCount = new AtomicLong(); private final AtomicLong pendingSubscribersCount = new AtomicLong(); private final AtomicBoolean isAlive = new AtomicBoolean(); @@ -429,14 +450,26 @@ final class HttpClientImpl extends HttpClient implements Trackable { id = CLIENT_IDS.incrementAndGet(); dbgTag = "HttpClientImpl(" + id +")"; localAddr = builder.localAddr; - if (builder.sslContext == null) { + version = requireNonNullElse(builder.version, Version.HTTP_2); + sslContext = requireNonNullElseGet(builder.sslContext, () -> { try { - sslContext = SSLContext.getDefault(); + return SSLContext.getDefault(); } catch (NoSuchAlgorithmException ex) { throw new UncheckedIOException(new IOException(ex)); } - } else { - sslContext = builder.sslContext; + }); + final boolean sslCtxSupportedForH3 = isQuicCompatible(sslContext); + if (version == Version.HTTP_3 && !sslCtxSupportedForH3) { + throw new UncheckedIOException(new UnsupportedProtocolVersionException( + "HTTP3 is not supported")); + } + sslParams = requireNonNullElseGet(builder.sslParams, sslContext::getDefaultSSLParameters); + boolean sslParamsSupportedForH3 = sslParams.getProtocols() == null + || sslParams.getProtocols().length == 0 + || isQuicCompatible(sslParams); + if (version == Version.HTTP_3 && !sslParamsSupportedForH3) { + throw new UncheckedIOException(new UnsupportedProtocolVersionException( + "HTTP3 is not supported - TLSv1.3 isn't configured on SSLParameters")); } Executor ex = builder.executor; if (ex == null) { @@ -450,7 +483,6 @@ final class HttpClientImpl extends HttpClient implements Trackable { this::onSubmitFailure); facadeRef = new WeakReference<>(facadeFactory.createFacade(this)); implRef = new WeakReference<>(this); - client2 = new Http2ClientImpl(this); cookieHandler = builder.cookieHandler; connectTimeout = builder.connectTimeout; followRedirects = builder.followRedirects == null ? @@ -462,17 +494,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { debug.log("proxySelector is %s (user-supplied=%s)", this.proxySelector, userProxySelector != null); authenticator = builder.authenticator; - if (builder.version == null) { - version = HttpClient.Version.HTTP_2; - } else { - version = builder.version; - } - if (builder.sslParams == null) { - sslParams = getDefaultParams(sslContext); - } else { - sslParams = builder.sslParams; - } + boolean h3Supported = sslCtxSupportedForH3 && sslParamsSupportedForH3; + registry = new AltServicesRegistry(id); connections = new ConnectionPool(id); + client2 = new Http2ClientImpl(this); + client3 = h3Supported ? new Http3ClientImpl(this) : null; connections.start(); timeouts = new TreeSet<>(); try { @@ -518,6 +544,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { client2.stop(); // make sure all subscribers are completed closeSubscribers(); + // close client3 + if (client3 != null) { + // close client3 + client3.stop(); + } // close TCP connection if any are still opened openedConnections.forEach(this::closeConnection); // shutdown the executor if needed @@ -610,11 +641,6 @@ final class HttpClientImpl extends HttpClient implements Trackable { return isStarted.get() && !isAlive.get(); } - private static SSLParameters getDefaultParams(SSLContext ctx) { - SSLParameters params = ctx.getDefaultSSLParameters(); - return params; - } - // Returns the facade that was returned to the application code. // May be null if that facade is no longer referenced. final HttpClientFacade facade() { @@ -664,12 +690,14 @@ final class HttpClientImpl extends HttpClient implements Trackable { final long count = pendingOperationCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.decrementAndGet(); final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.get(); final long webSocketCount = pendingWebSocketCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { selmgr.wakeupSelector(); } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -681,10 +709,35 @@ final class HttpClientImpl extends HttpClient implements Trackable { return pendingOperationCount.incrementAndGet(); } + // Increments the pendingHttp3StreamCount and pendingOperationCount. + final long h3StreamReference() { + pendingHttp3StreamCount.incrementAndGet(); + return pendingOperationCount.incrementAndGet(); + } + // Decrements the pendingHttp2StreamCount and pendingOperationCount. final long streamUnreference() { final long count = pendingOperationCount.decrementAndGet(); final long http2Count = pendingHttp2StreamCount.decrementAndGet(); + final long http3Count = pendingHttp3StreamCount.get(); + final long httpCount = pendingHttpOperationsCount.get(); + final long webSocketCount = pendingWebSocketCount.get(); + if (count == 0 && facadeRef.refersTo(null)) { + selmgr.wakeupSelector(); + } + assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; + assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; + assert webSocketCount >= 0 : "count of WS operations < 0"; + assert count >= 0 : "count of pending operations < 0"; + return count; + } + + // Decrements the pendingHttp3StreamCount and pendingOperationCount. + final long h3StreamUnreference() { + final long count = pendingOperationCount.decrementAndGet(); + final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.get(); final long webSocketCount = pendingWebSocketCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { @@ -692,6 +745,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -709,11 +763,13 @@ final class HttpClientImpl extends HttpClient implements Trackable { final long webSocketCount = pendingWebSocketCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.get(); final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { selmgr.wakeupSelector(); } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -732,6 +788,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { final AtomicLong requestCount; final AtomicLong httpCount; final AtomicLong http2Count; + final AtomicLong http3Count; final AtomicLong websocketCount; final AtomicLong operationsCount; final AtomicLong connnectionsCount; @@ -744,6 +801,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { HttpClientTracker(AtomicLong request, AtomicLong http, AtomicLong http2, + AtomicLong http3, AtomicLong ws, AtomicLong ops, AtomicLong conns, @@ -756,6 +814,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { this.requestCount = request; this.httpCount = http; this.http2Count = http2; + this.http3Count = http3; this.websocketCount = ws; this.operationsCount = ops; this.connnectionsCount = conns; @@ -787,6 +846,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { @Override public long getOutstandingHttp2Streams() { return http2Count.get(); } @Override + public long getOutstandingHttp3Streams() { return http3Count.get(); } + @Override public long getOutstandingWebSocketOperations() { return websocketCount.get(); } @@ -811,6 +872,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { pendingHttpRequestCount, pendingHttpOperationsCount, pendingHttp2StreamCount, + pendingHttp3StreamCount, pendingWebSocketCount, pendingOperationCount, pendingTCPConnectionCount, @@ -866,6 +928,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { return Thread.currentThread() == selmgr; } + AltServicesRegistry registry() { return registry; } + boolean isSelectorClosed() { return selmgr.isClosed(); } @@ -878,6 +942,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { return client2; } + Optional client3() { + return Optional.ofNullable(client3); + } + private void debugCompleted(String tag, long startNanos, HttpRequest req) { if (debugelapsed.on()) { debugelapsed.log(tag + " elapsed " @@ -917,6 +985,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg); hcte.initCause(throwable); throw hcte; + } else if (throwable instanceof UnsupportedProtocolVersionException) { + var upve = new UnsupportedProtocolVersionException(msg); + upve.initCause(throwable); + throw upve; } else if (throwable instanceof HttpTimeoutException) { throw new HttpTimeoutException(msg); } else if (throwable instanceof ConnectException) { @@ -972,6 +1044,13 @@ final class HttpClientImpl extends HttpClient implements Trackable { return MinimalFuture.failedFuture(new IOException("closed")); } + final HttpClient.Version vers = userRequest.version().orElse(this.version()); + if (vers == Version.HTTP_3 && client3 == null + && userRequest.getOption(H3_DISCOVERY).orElse(null) == HTTP_3_URI_ONLY) { + // HTTP3 isn't supported by this client + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "HTTP3 is not supported")); + } // should not happen, unless the selector manager has // exited abnormally if (selmgr.isClosed()) { @@ -1095,8 +1174,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { } IOException selectorClosedException() { - var io = new IOException("selector manager closed"); - var cause = errorRef.get(); + final var cause = errorRef.get(); + final String msg = cause == null + ? "selector manager closed" + : "selector manager closed due to: " + cause; + final var io = new IOException(msg); if (cause != null) { io.initCause(cause); } @@ -1181,6 +1263,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { } // double check after closing abortPendingRequests(owner, t); + var client3 = owner.client3; + if (client3 != null) { + client3.abort(t); + } IOException io = toAbort.isEmpty() ? null : selectorClosedException(); @@ -1456,8 +1542,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { String keyInterestOps = key.isValid() ? "key.interestOps=" + Utils.interestOps(key) : "invalid key"; return String.format("channel registered with selector, %s, sa.interestOps=%s", - keyInterestOps, - Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); + keyInterestOps, + Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); } catch (Throwable t) { return String.valueOf(t); } @@ -1627,8 +1713,12 @@ final class HttpClientImpl extends HttpClient implements Trackable { return Optional.ofNullable(connectTimeout); } - Optional idleConnectionTimeout() { - return Optional.ofNullable(getIdleConnectionTimeout()); + Optional idleConnectionTimeout(Version version) { + return switch (version) { + case HTTP_2 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H2); + case HTTP_3 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H3); + case HTTP_1_1 -> timeoutDuration(KEEP_ALIVE_TIMEOUT); + }; } @Override @@ -1755,7 +1845,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { // error from here - but in this case there's not much we // could do anyway. Just let it flow... if (failed == null) failed = e; - else failed.addSuppressed(e); + else Utils.addSuppressed(failed, e); Log.logTrace("Failed to handle event {0}: {1}", event, e); } } @@ -1799,10 +1889,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { return sslBufferSupplier; } - private Duration getIdleConnectionTimeout() { - if (IDLE_CONNECTION_TIMEOUT >= 0) - return Duration.ofSeconds(IDLE_CONNECTION_TIMEOUT); - return null; + private Optional timeoutDuration(long seconds) { + if (seconds >= 0) { + return Optional.of(Duration.ofSeconds(seconds)); + } + return Optional.empty(); } private static long getTimeoutProp(String prop, long def) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java index 07cfc4dbdf6..0219b0960d7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.http.HttpResponse; import java.nio.ByteBuffer; +import java.nio.channels.NetworkChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; import java.util.Comparator; @@ -57,7 +58,10 @@ import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter; import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Utils; + +import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; import static jdk.internal.net.http.common.Utils.ProxyHeaders; /** @@ -69,12 +73,13 @@ import static jdk.internal.net.http.common.Utils.ProxyHeaders; * PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server * AsyncSSLConnection: TLS channel direct to server * AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel + * HttpQuicConnection: direct QUIC connection to server */ abstract class HttpConnection implements Closeable { final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG); static final Logger DEBUG_LOGGER = Utils.getDebugLogger( - () -> "HttpConnection(SocketTube(?))", Utils.DEBUG); + () -> "HttpConnection", Utils.DEBUG); public static final Comparator COMPARE_BY_ID = Comparator.comparing(HttpConnection::id); @@ -112,8 +117,8 @@ abstract class HttpConnection implements Closeable { this.label = label; } - private static String nextLabel() { - return "" + LABEL_COUNTER.incrementAndGet(); + private static String nextLabel(String prefix) { + return prefix + LABEL_COUNTER.incrementAndGet(); } /** @@ -198,9 +203,17 @@ abstract class HttpConnection implements Closeable { abstract InetSocketAddress proxy(); /** Tells whether, or not, this connection is open. */ - final boolean isOpen() { + boolean isOpen() { return channel().isOpen() && - (connected() ? !getConnectionFlow().isFinished() : true); + (connected() ? !isFlowFinished() : true); + } + + /** + * {@return {@code true} if the {@linkplain #getConnectionFlow() + * connection flow} is {@linkplain FlowTube#isFinished() finished}. + */ + boolean isFlowFinished() { + return getConnectionFlow().isFinished(); } /** @@ -232,13 +245,17 @@ abstract class HttpConnection implements Closeable { * still open, and the method returns true. * @return true if the channel appears to be still open. */ - final boolean checkOpen() { + boolean checkOpen() { if (isOpen()) { try { // channel is non blocking - int read = channel().read(ByteBuffer.allocate(1)); - if (read == 0) return true; - close(); + if (channel() instanceof SocketChannel channel) { + int read = channel.read(ByteBuffer.allocate(1)); + if (read == 0) return true; + close(); + } else { + return channel().isOpen(); + } } catch (IOException x) { debug.log("Pooled connection is no longer operational: %s", x.toString()); @@ -294,6 +311,7 @@ abstract class HttpConnection implements Closeable { * is one of the following: * {@link PlainHttpConnection} * {@link PlainTunnelingConnection} + * {@link HttpQuicConnection} * * The returned connection, if not from the connection pool, must have its, * connect() or connectAsync() method invoked, which ( when it completes @@ -301,6 +319,7 @@ abstract class HttpConnection implements Closeable { */ public static HttpConnection getConnection(InetSocketAddress addr, HttpClientImpl client, + Exchange exchange, HttpRequestImpl request, Version version) { // The default proxy selector may select a proxy whose address is @@ -322,18 +341,27 @@ abstract class HttpConnection implements Closeable { return getPlainConnection(addr, proxy, request, client); } } else { // secure - if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool + if (version == HTTP_1_1) { // only HTTP/1.1 connections are in the pool c = pool.getConnection(true, addr, proxy); } if (c != null && c.isOpen()) { - final HttpConnection conn = c; - if (DEBUG_LOGGER.on()) - DEBUG_LOGGER.log(conn.getConnectionFlow() - + ": SSL connection retrieved from HTTP/1.1 pool"); + if (DEBUG_LOGGER.on()) { + DEBUG_LOGGER.log(c.getConnectionFlow() + + ": SSL connection retrieved from HTTP/1.1 pool"); + } return c; + } else if (version == HTTP_3 && client.client3().isPresent()) { + // We only come here after we have checked the HTTP/3 connection pool, + // and if the client config supports HTTP/3 + if (DEBUG_LOGGER.on()) + DEBUG_LOGGER.log("Attempting to get an HTTP/3 connection"); + return HttpQuicConnection.getHttpQuicConnection(addr, proxy, request, exchange, client); } else { + assert !request.isHttp3Only(version); // should have failed before String[] alpn = null; if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) { + // We only come here after we have checked the HTTP/2 connection pool. + // We will not negotiate HTTP/2 if we don't have the appropriate TLS version alpn = new String[] { Alpns.H2, Alpns.HTTP_1_1 }; } return getSSLConnection(addr, proxy, alpn, request, client); @@ -346,7 +374,7 @@ abstract class HttpConnection implements Closeable { String[] alpn, HttpRequestImpl request, HttpClientImpl client) { - final String label = nextLabel(); + final String label = nextLabel("tls:"); final Origin originServer; try { originServer = Origin.from(request.uri()); @@ -433,7 +461,7 @@ abstract class HttpConnection implements Closeable { InetSocketAddress proxy, HttpRequestImpl request, HttpClientImpl client) { - final String label = nextLabel(); + final String label = nextLabel("tcp:"); final Origin originServer; try { originServer = Origin.from(request.uri()); @@ -483,7 +511,7 @@ abstract class HttpConnection implements Closeable { /* Tells whether or not this connection is a tunnel through a proxy */ boolean isTunnel() { return false; } - abstract SocketChannel channel(); + abstract NetworkChannel channel(); final InetSocketAddress address() { return address; @@ -516,6 +544,19 @@ abstract class HttpConnection implements Closeable { close(); } + /** + * {@return the underlying connection flow, if applicable} + * + * @apiNote + * TCP based protocols like HTTP/1.1 and HTTP/2 are built on + * top of a {@linkplain FlowTube bidirectional connection flow}. + * On the other hand, Quic based protocol like HTTP/3 are + * multiplexed at the Quic level, and therefore do not have + * a connection flow. + * + * @throws IllegalStateException if the underlying transport + * does not expose a single connection flow. + */ abstract FlowTube getConnectionFlow(); /** diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java new file mode 100644 index 00000000000..bbbe1157cdf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java @@ -0,0 +1,690 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.nio.channels.NetworkChannel; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.ConnectionPool.CacheKey; +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnection; + +import static jdk.internal.net.http.Http3ClientProperties.MAX_DIRECT_CONNECTION_TIMEOUT; +import static jdk.internal.net.http.common.Alpns.H3; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; +import static jdk.internal.net.http.quic.TerminationCause.appLayerException; + +/** + * An {@code HttpQuicConnection} models an HTTP connection over + * QUIC. + * The particulars of the HTTP/3 protocol are handled by the + * Http3Connection class. + */ +abstract class HttpQuicConnection extends HttpConnection { + + final Logger debug = Utils.getDebugLogger(this::quicDbgString); + + final QuicConnection quicConnection; + final ConnectionTerminator quicConnTerminator; + // the alt-service which was advertised, from some origin, for this connection co-ordinates. + // can be null, which indicates this wasn't created because of an alt-service + private final AltService sourceAltService; + // HTTP/2 MUST use TLS version 1.3 or higher for HTTP/3 over TLS + private static final Predicate testRequiredHTTP3TLSVersion = proto -> + proto.equals("TLSv1.3"); + + + HttpQuicConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client, + QuicConnection quicConnection, AltService sourceAltService) { + super(originServer, address, client, "quic:" + quicConnection.uniqueId()); + Objects.requireNonNull(quicConnection); + this.quicConnection = quicConnection; + this.quicConnTerminator = quicConnection.connectionTerminator(); + this.sourceAltService = sourceAltService; + } + + /** + * A HTTP QUIC connection could be created due to an alt-service that was advertised + * from some origin. This method returns that source alt-service if there was one. + * @return The source alt-service if present + */ + Optional getSourceAltService() { + return Optional.ofNullable(this.sourceAltService); + } + + @Override + public List getSNIServerNames() { + final SSLParameters sslParams = this.quicConnection.getTLSEngine().getSSLParameters(); + if (sslParams == null) { + return List.of(); + } + final List sniServerNames = sslParams.getServerNames(); + if (sniServerNames == null) { + return List.of(); + } + return List.copyOf(sniServerNames); + } + + final String quicDbgString() { + String tag = dbgTag; + if (tag == null) tag = dbgTag = "Http" + quicConnection.dbgTag(); + return tag; + } + + /** + * Initiates the connect phase. + * + * Returns a CompletableFuture that completes when the underlying + * TCP connection has been established or an error occurs. + */ + public abstract CompletableFuture connectAsync(Exchange exchange); + + private volatile boolean connected; + /** + * Finishes the connection phase. + * + * Returns a CompletableFuture that completes when any additional, + * type specific, setup has been done. Must be called after connectAsync. + */ + public CompletableFuture finishConnect() { + this.connected = true; + return MinimalFuture.completedFuture(null); + } + + /** Tells whether, or not, this connection is connected to its destination. */ + boolean connected() { + return connected; + } + + /** Tells whether, or not, this connection is secure ( over SSL ) */ + final boolean isSecure() { return true; } // QUIC is secure + + /** + * Tells whether, or not, this connection is proxied. + * Returns true for tunnel connections, or clear connection to + * any host through proxy. + */ + final boolean isProxied() { return false;} // Proxy not supported + + /** + * Returns the address of the proxy used by this connection. + * Returns the proxy address for tunnel connections, or + * clear connection to any host through proxy. + * Returns {@code null} otherwise. + */ + final InetSocketAddress proxy() { return null; } // Proxy not supported + + /** + * This method throws an {@link UnsupportedOperationException} + */ + @Override + final HttpPublisher publisher() { + throw new UnsupportedOperationException("no publisher for a quic connection"); + } + + QuicConnection quicConnection() { + return quicConnection; + } + + /** + * Returns true if the given client's SSL parameter protocols contains at + * least one TLS version that HTTP/3 requires. + */ + private static boolean hasRequiredHTTP3TLSVersion(HttpClient client) { + String[] protos = client.sslParameters().getProtocols(); + if (protos != null) { + return Arrays.stream(protos).anyMatch(testRequiredHTTP3TLSVersion); + } else { + return false; + } + } + + /** + * Called when the HTTP/3 connection is established, either successfully or + * unsuccessfully + * @param connection the HTTP/3 connection, if successful, or null, otherwise + * @param throwable the exception encountered, if unsuccessful + */ + public abstract void connectionEstablished(Http3Connection connection, + Throwable throwable); + + /** + * A functional interface used to update the Alternate Service Registry + * after a direct connection attempt. + */ + @FunctionalInterface + private interface DirectConnectionUpdater { + /** + * This method may update the HttpClient registry, or + * {@linkplain Http3ClientImpl#noH3(String) record the unsuccessful} + * direct connection attempt. + * + * @param conn the connection or null + * @param throwable the exception or null + */ + void onConnectionEstablished( + Http3Connection conn, Throwable throwable); + + /** + * Does nothing + * @param conn the connection + * @param throwable the exception + */ + static void noUpdate( + Http3Connection conn, Throwable throwable) { + } + } + + /** + * This method create and return a new unconnected HttpQuicConnection, + * wrapping a {@link QuicConnection}. May return {@code null} if + * HTTP/3 is not supported with the given parameters. For instance, + * if TLSv1.3 isn't available/enabled in the client's SSLParameters, + * or if ALT_SERVICE is required but no alt service is found. + * + * @param addr the HTTP/3 peer endpoint address, if direct connection + * @param proxy the proxy address, if a proxy is used, in which case this + * method will return {@code null} as proxying is not supported + * with HTTP/3 + * @param request the request for which the connection is being created + * @param exchange the exchange for which the connection is being created + * @param client the HttpClientImpl instance + * @return A new HttpQuicConnection or {@code null} + */ + public static HttpQuicConnection getHttpQuicConnection(final InetSocketAddress addr, + final InetSocketAddress proxy, + final HttpRequestImpl request, + final Exchange exchange, + final HttpClientImpl client) { + if (!client.client3().isPresent()) { + if (Log.http3()) { + Log.logHttp3("HTTP3 isn't supported by the client"); + } + return null; + } + + final Http3ClientImpl h3client = client.client3().get(); + // HTTP_3 with proxy not supported; In this case we will downgrade + // to using HTTP/2 + var debug = h3client.debug(); + var where = "HttpQuicConnection.getHttpQuicConnection"; + if (proxy != null || !hasRequiredHTTP3TLSVersion(client)) { + if (debug.on()) + debug.log("%s: proxy required or SSL version mismatch", where); + return null; + } + + assert request.secure(); + // Question: Do we need this scaffolding? + // I mean - could Http3Connection and HttpQuicConnection be the same + // object? + // Answer: Http3Connection models an established connection which is + // ready to be used. + // HttpQuicConnection serves at establishing a new Http3Connection + // => Http3Connection is pooled, HttpQuicConnection is not. + // => Do we need HttpQuicConnection vs QuicConnection? + // => yes: HttpQuicConnection can access all package protected + // APIs in HttpConnection & al + // QuicConnection is in the quic subpackage. + // HttpQuicConnection makes the necessary adaptation between + // HttpConnection and QuicConnection. + + // find whether we have an alternate service access point for HTTP/3 + // if we do, create a new QuicConnection and a new Http3Connection over it. + var uri = request.uri(); + var config = request.http3Discovery(); + if (debug.on()) { + debug.log("Checking ALT-SVC regardless of H3_DISCOVERY settings"); + } + // we only support H3 right now + var altSvc = client.registry() + .lookup(uri, H3::equals) + .findFirst().orElse(null); + Optional directTimeout = Optional.empty(); + final boolean advertisedAltSvc = altSvc != null && altSvc.wasAdvertised(); + logAltSvcFor(debug, uri, altSvc, where); + switch (config) { + case ALT_SVC: { + if (!advertisedAltSvc) { + // fallback to HTTP/2 + if (altSvc != null) { + if (Log.altsvc()) { + Log.logAltSvc("{0}: Cannot use unadvertised AltService: {1}", + config, altSvc); + } + } + return null; + } + assert altSvc != null && altSvc.wasAdvertised(); + break; + } + // attempt direct connection if HTTP/3 only + case HTTP_3_URI_ONLY: { + if (advertisedAltSvc && !altSvc.originHasSameAuthority()) { + if (Log.altsvc()) { + Log.logAltSvc("{0}: Cannot use advertised AltService: {1}", + config, altSvc); + } + altSvc = null; + } + assert altSvc == null || altSvc.originHasSameAuthority(); + break; + } + default: { + // if direct connection already attempted and failed, + // fallback to HTTP/2 + if (altSvc == null && h3client.hasNoH3(uri.getRawAuthority())) { + return null; + } + if (!advertisedAltSvc) { + // directTimeout is only used for happy eyeball + Duration def = Duration.ofMillis(MAX_DIRECT_CONNECTION_TIMEOUT); + Duration timeout = client.connectTimeout() + .filter(d -> d.compareTo(def) <= 0) + .orElse(def); + directTimeout = Optional.of(timeout); + } + break; + } + } + + if (altSvc != null) { + assert H3.equals(altSvc.alpn()); + Log.logAltSvc("{0}: Using AltService for {1}: {2}", + config, uri.getRawAuthority(), altSvc); + } + if (debug.on()) { + debug.log("%s: creating QuicConnection for: %s", where, uri); + } + final QuicConnection quicConnection = (altSvc != null) ? + h3client.quicClient().createConnectionFor(altSvc) : + h3client.quicClient().createConnectionFor(addr, new String[] {H3}); + if (debug.on()) debug.log("%s: QuicConnection: %s", where, quicConnection); + final DirectConnectionUpdater onConnectFinished = advertisedAltSvc + ? DirectConnectionUpdater::noUpdate + : (c,t) -> registerUnadvertised(client, uri, addr, c, t); + // Note: we could get rid of the updater by introducing + // H3DirectQuicConnectionImpl extends H3QuicConnectionImpl + HttpQuicConnection httpQuicConn = new H3QuicConnectionImpl(Origin.from(request.uri()), addr, client, + quicConnection, onConnectFinished, directTimeout, altSvc); + // if we created a connection and if that connection is to an (advertised) alt service then + // we setup the Exchange's request to include the "alt-used" header to refer to the + // alt service that was used (section 5, RFC-7838) + if (httpQuicConn != null && altSvc != null && advertisedAltSvc) { + exchange.request().setSystemHeader("alt-used", altSvc.authority()); + } + return httpQuicConn; + } + + private static void logAltSvcFor(Logger debug, URI uri, AltService altSvc, String where) { + if (altSvc == null) { + if (Log.altsvc()) { + Log.logAltSvc("No AltService found for {0}", uri.getRawAuthority()); + } else if (debug.on()) { + debug.log("%s: No ALT-SVC for %s", where, uri.getRawAuthority()); + } + } else { + if (debug.on()) debug.log("%s: ALT-SVC: %s", where, altSvc); + } + } + + static void registerUnadvertised(final HttpClientImpl client, + final URI requestURI, + final InetSocketAddress destAddr, + final Http3Connection connection, + final Throwable t) { + if (t == null && connection != null) { + // There is an h3 endpoint at the given origin: update the registry + final Origin origin = connection.connection().getOriginServer(); + assert origin != null : "origin server is null on connection: " + + connection.connection(); + assert origin.port() == destAddr.getPort(); + var id = new AltService.Identity(H3, origin.host(), origin.port()); + client.registry().registerUnadvertised(id, origin, connection.connection()); + return; + } + if (t != null) { + assert client.client3().isPresent() : "HTTP3 isn't supported by the client"; + final URI originURI = requestURI.resolve("/"); + // record that there is no h3 at the given origin + client.client3().get().noH3(originURI.getRawAuthority()); + } + } + + // TODO: we could probably merge H3QuicConnectionImpl with HttpQuicConnection now + static class H3QuicConnectionImpl extends HttpQuicConnection { + private final Optional directTimeout; + private final DirectConnectionUpdater connFinishedAction; + H3QuicConnectionImpl(Origin originServer, + InetSocketAddress address, + HttpClientImpl client, + QuicConnection quic, + DirectConnectionUpdater connFinishedAction, + Optional directTimeout, + AltService sourceAltService) { + super(originServer, address, client, quic, sourceAltService); + this.directTimeout = directTimeout; + this.connFinishedAction = connFinishedAction; + } + + @Override + public CompletableFuture connectAsync(Exchange exchange) { + var request = exchange.request(); + var uri = request.uri(); + // Adapt HandshakeCF to CompletableFuture + CompletableFuture> handshakeCfCf = + quicConnection.startHandshake() + .handle((r, t) -> { + if (t == null) { + // successful handshake + return MinimalFuture.completedFuture(r); + } + final TerminationCause terminationCause = quicConnection.terminationCause(); + final boolean appLayerTermination = terminationCause != null + && terminationCause.isAppLayer(); + // QUIC connection handshake failed. we now decide whether we should + // unregister the alt-service (if any) that was the source of this + // connection attempt. + // + // handshake could have failed for one of several reasons, some of them: + // - something at QUIC layer caused the failure (either some internal + // exception or protocol error or QUIC TLS error) + // - or the app layer, through the HttpClient/HttpConnection + // could have triggered a connection close. + // + // we unregister the alt-service (if any) only if the termination cause + // originated in the QUIC layer. An app layer termination cause doesn't + // necessarily mean that the alt-service isn't valid for subsequent use. + if (!appLayerTermination && this.getSourceAltService().isPresent()) { + final AltService altSvc = this.getSourceAltService().get(); + if (debug.on()) { + debug.log("connection attempt to an alternate service at " + + altSvc.authority() + " failed during handshake: " + t); + } + client().registry().markInvalid(this.getSourceAltService().get()); + // fail with ConnectException to allow the request to potentially + // be retried on a different connection + final ConnectException connectException = new ConnectException( + "QUIC connection handshake to an alternate service failed"); + connectException.initCause(t); + return MinimalFuture.failedFuture(connectException); + } else { + // alt service wasn't the cause of this failed connection attempt. + // return a failed future with the original cause + return MinimalFuture.failedFuture(t); + } + }) + .thenApply((handshakeCompletion) -> { + if (handshakeCompletion.isCompletedExceptionally()) { + return MinimalFuture.failedFuture(handshakeCompletion.exceptionNow()); + } + return MinimalFuture.completedFuture(null); + }); + + // In case of direct connection, set up a timeout on the handshakeReachedPeerCf, + // and arrange for it to complete the handshakeCfCf above with a timeout in + // case that timeout expires... + if (directTimeout.isPresent()) { + debug.log("setting up quic direct connect timeout: " + directTimeout.get().toMillis()); + var handshakeReachedPeerCf = quicConnection.handshakeReachedPeer(); + CompletableFuture> fxi2 = handshakeReachedPeerCf + .thenApply((unused) -> MinimalFuture.completedFuture(null)); + fxi2 = fxi2.completeOnTimeout( + MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic handshake timeout")), + directTimeout.get().toMillis(), TimeUnit.MILLISECONDS); + fxi2.handleAsync((r, t) -> { + if (t != null) { + var cause = Utils.getCompletionCause(t); + // arrange for handshakeCfCf to timeout + handshakeCfCf.completeExceptionally(cause); + } + if (r.isCompletedExceptionally()) { + var cause = Utils.getCompletionCause(r.exceptionNow()); + // arrange for handshakeCfCf to timeout + handshakeCfCf.completeExceptionally(cause); + } + return r; + }, exchange.parentExecutor.safeDelegate()); + } + + Optional timeout = client().connectTimeout(); + CompletableFuture> fxi = handshakeCfCf; + + // In case of connection timeout, set up a timeout on the handshakeCfCf. + // Note: this is a different timeout than the direct connection timeout. + if (timeout.isPresent()) { + // In case of timeout we need to close the quic connection + debug.log("setting up quic connect timeout: " + timeout.get().toMillis()); + fxi = handshakeCfCf.completeOnTimeout( + MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic connect timeout")), + timeout.get().toMillis(), TimeUnit.MILLISECONDS); + } + + // If we have set up any timeout, arrange to close the quicConnection + // if one of the timeout expires + if (timeout.isPresent() || directTimeout.isPresent()) { + fxi = fxi.handleAsync(this::handleTimeout, exchange.parentExecutor.safeDelegate()); + } + return fxi.thenCompose(Function.identity()); + } + + @Override + public void connectionEstablished(Http3Connection connection, + Throwable throwable) { + connFinishedAction.onConnectionEstablished(connection, throwable); + } + + private CompletableFuture handleTimeout(CompletableFuture r, Throwable t) { + if (t != null) { + if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) { + debug.log("Timeout expired: " + te); + close(H3_NO_ERROR.code(), "timeout expired", te); + return MinimalFuture.failedFuture(te); + } + return MinimalFuture.failedFuture(t); + } else if (r.isCompletedExceptionally()) { + t = r.exceptionNow(); + if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) { + debug.log("Completed in timeout: " + te); + close(H3_NO_ERROR.code(), "timeout expired", te); + } + } + return r; + } + + + @Override + NetworkChannel /* DatagramChannel */ channel() { + // Note: revisit this + // - don't return a new instance each time + // - see if we could avoid exposing + // the channel in the first place + H3QuicConnectionImpl self = this; + return new NetworkChannel() { + @Override + public NetworkChannel bind(SocketAddress local) throws IOException { + throw new UnsupportedOperationException("no bind for a quic connection"); + } + + @Override + public SocketAddress getLocalAddress() throws IOException { + return quicConnection.localAddress(); + } + + @Override + public NetworkChannel setOption(SocketOption name, T value) throws IOException { + return this; + } + + @Override + public T getOption(SocketOption name) throws IOException { + return null; + } + + @Override + public Set> supportedOptions() { + return Set.of(); + } + + @Override + public boolean isOpen() { + return quicConnection.isOpen(); + } + + @Override + public void close() throws IOException { + self.close(); + } + }; + } + + @Override + CacheKey cacheKey() { + return null; + } + + // close with H3_NO_ERROR + @Override + public final void close() { + close(H3_NO_ERROR.code(), "connection closed", null); + } + + @Override + void close(final Throwable cause) { + close(H3_INTERNAL_ERROR.code(), null, cause); + } + } + + /* Tells whether this connection is a tunnel through a proxy */ + boolean isTunnel() { return false; } + + abstract NetworkChannel /* DatagramChannel */ channel(); + + abstract ConnectionPool.CacheKey cacheKey(); + + /** + * Closes the underlying transport connection with + * the given {@code connCloseCode} code. This will be considered a application + * layer close and will generate a {@code ConnectionCloseFrame} + * of type {@code 0x1d} as the cause of the termination. + * + * @param connCloseCode the connection close code + * @param logMsg the message to be included in the logs as + * the cause of the connection termination. can be null. + * @param closeCause the underlying cause of the connection termination. can be null, + * in which case just the {@code error} will be recorded as the + * cause of the connection termination. + */ + final void close(final long connCloseCode, final String logMsg, + final Throwable closeCause) { + final TerminationCause terminationCause; + if (closeCause == null) { + terminationCause = appLayerClose(connCloseCode); + } else { + terminationCause = appLayerException(connCloseCode, closeCause); + } + // set the log message only if non-null, else let it default to internal + // implementation sensible default + if (logMsg != null) { + terminationCause.loggedAs(logMsg); + } + quicConnTerminator.terminate(terminationCause); + } + + abstract void close(final Throwable t); + + /** + * {@inheritDoc} + * + * @implSpec + * Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not + * built on a single connection flow, since multiplexing is + * provided by the lower layer. Therefore, the higher HTTP + * layer should never call {@code getConnectionFlow()} on an + * {@link HttpQuicConnection}. As a consequence, this method + * always throws {@link IllegalStateException} unconditionally. + * + * @return nothing: this method always throw {@link IllegalStateException} + * + * @throws IllegalStateException always + */ + @Override + final FlowTube getConnectionFlow() { + throw new IllegalStateException( + "An HTTP/3 connection does not expose " + + "a single connection flow"); + } + + /** + * Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not + * built on a single connection flow, since multiplexing is + * provided by the lower layer. This method instead will + * return {@code true} if the underlying quic connection + * has been terminated, either exceptionally or normally. + * + * @return {@code true} if the underlying Quic connection + * has been terminated. + */ + @Override + boolean isFlowFinished() { + return !quicConnection().isOpen(); + } + + @Override + public String toString() { + return quicDbgString(); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java index a495fcce1ee..c68965626b7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java @@ -26,12 +26,18 @@ package jdk.internal.net.http; import java.net.URI; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpOption; import java.time.Duration; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; +import java.util.Set; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.common.Utils; @@ -51,6 +57,10 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { private BodyPublisher bodyPublisher; private volatile Optional version; private Duration duration; + private final Map, Object> options = new HashMap<>(); + + private static final Set> supportedOptions = + Set.of(HttpOption.H3_DISCOVERY); public HttpRequestBuilderImpl(URI uri) { requireNonNull(uri, "uri must be non-null"); @@ -100,6 +110,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { b.uri = uri; b.duration = duration; b.version = version; + b.options.putAll(Map.copyOf(options)); return b; } @@ -158,6 +169,19 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { return this; } + @Override + public Builder setOption(HttpOption option, T value) { + Objects.requireNonNull(option, "option"); + if (value == null) options.remove(option); + else if (supportedOptions.contains(option)) { + if (!option.type().isInstance(value)) { + throw newIAE("Illegal value type %s for %s", value, option); + } + options.put(option, value); + } // otherwise just ignore the option + return this; + } + HttpHeadersBuilder headersBuilder() { return headersBuilder; } URI uri() { return uri; } @@ -170,6 +194,8 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { Optional version() { return version; } + Map, Object> options() { return options; } + @Override public HttpRequest.Builder GET() { return method0("GET", null); @@ -245,4 +271,30 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { Duration timeout() { return duration; } + public static Map, Object> copySupportedOptions(HttpRequest request) { + Objects.requireNonNull(request, "request"); + if (request instanceof ImmutableHttpRequest ihr) { + // already checked and immutable + return ihr.options(); + } + Map, Object> options = new HashMap<>(); + for (HttpOption option : supportedOptions) { + var val = request.getOption(option); + if (!val.isPresent()) continue; + options.put(option, option.type().cast(val.get())); + } + return Map.copyOf(options); + } + + public static Map, Object> copySupportedOptions(Map, Object> options) { + Objects.requireNonNull(options, "option"); + Map, Object> result = new HashMap<>(); + for (HttpOption option : supportedOptions) { + var val = options.get(option); + if (val == null) continue; + result.put(option, option.type().cast(val)); + } + return Map.copyOf(result); + } + } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java index 81c693ea192..00730baa0ce 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java @@ -31,9 +31,13 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.time.Duration; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient; @@ -46,6 +50,7 @@ import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.websocket.WebSocketRequest; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.Authenticator.RequestorType.SERVER; import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS; import static jdk.internal.net.http.common.Utils.ProxyHeaders; @@ -65,6 +70,8 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { private volatile boolean isWebSocket; private final Duration timeout; // may be null private final Optional version; + // An alternative would be to have one field per supported option + private final Map, Object> options; private volatile boolean userSetAuthorization; private volatile boolean userSetProxyAuthorization; @@ -92,6 +99,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.requestPublisher = builder.bodyPublisher(); // may be null this.timeout = builder.timeout(); this.version = builder.version(); + this.options = Map.copyOf(builder.options()); this.authority = null; } @@ -110,12 +118,13 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { "uri must be non null"); Duration timeout = request.timeout().orElse(null); this.method = method == null ? "GET" : method; + this.options = HttpRequestBuilderImpl.copySupportedOptions(request); this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER); - if (request instanceof HttpRequestImpl) { + if (request instanceof HttpRequestImpl impl) { // all cases exception WebSocket should have a new system headers - this.isWebSocket = ((HttpRequestImpl) request).isWebSocket; + this.isWebSocket = impl.isWebSocket; if (isWebSocket) { - this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder; + this.systemHeadersBuilder = impl.systemHeadersBuilder; } else { this.systemHeadersBuilder = new HttpHeadersBuilder(); } @@ -199,6 +208,19 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.timeout = other.timeout; this.version = other.version(); this.authority = null; + this.options = other.optionsFor(this.uri); + } + + private Map, Object> optionsFor(URI uri) { + if (this.uri == uri || Objects.equals(this.uri.getRawAuthority(), uri.getRawAuthority())) { + return options; + } + // preserve config if version is HTTP/3 + if (version.orElse(null) == Version.HTTP_3) { + Http3DiscoveryMode h3DiscoveryMode = (Http3DiscoveryMode)options.get(H3_DISCOVERY); + if (h3DiscoveryMode != null) return Map.of(H3_DISCOVERY, h3DiscoveryMode); + } + return Map.of(); } private BodyPublisher publisher(HttpRequestImpl other) { @@ -234,12 +256,26 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { // What we want to possibly upgrade is the tunneled connection to the // target server (so not the CONNECT request itself) this.version = Optional.of(HttpClient.Version.HTTP_1_1); + this.options = Map.of(); } final boolean isConnect() { return "CONNECT".equalsIgnoreCase(method); } + final boolean isHttp3Only(Version version) { + return version == Version.HTTP_3 && http3Discovery() == HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + } + + final Http3DiscoveryMode http3Discovery() { + // see if discovery mode is set on the request + final var h3Discovery = getOption(H3_DISCOVERY); + // if no explicit discovery mode is set, then default to "ANY" + // irrespective of whether the HTTP/3 version may have been + // set on the HttpClient or the HttpRequest + return h3Discovery.orElse(Http3DiscoveryMode.ANY); + } + /** * Creates a HttpRequestImpl from the given set of Headers and the associated * "parent" request. Fields not taken from the headers are taken from the @@ -276,6 +312,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.timeout = parent.timeout; this.version = parent.version; this.authority = null; + this.options = parent.options; } @Override @@ -399,6 +436,11 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { @Override public Optional version() { return version; } + @Override + public Optional getOption(HttpOption option) { + return Optional.ofNullable(option.type().cast(options.get(option))); + } + @Override public void setSystemHeader(String name, String value) { systemHeadersBuilder.setHeader(name, value); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java index 1552cd40ede..ba5bea43078 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java @@ -41,14 +41,14 @@ import jdk.internal.net.http.websocket.RawChannel; /** * The implementation class for HttpResponse */ -class HttpResponseImpl implements HttpResponse, RawChannel.Provider { +final class HttpResponseImpl implements HttpResponse, RawChannel.Provider { final int responseCode; private final String connectionLabel; final HttpRequest initialRequest; - final Optional> previousResponse; + final HttpResponse previousResponse; // may be null; final HttpHeaders headers; - final Optional sslSession; + final SSLSession sslSession; // may be null final URI uri; final HttpClient.Version version; final RawChannelProvider rawChannelProvider; @@ -62,10 +62,10 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { this.responseCode = response.statusCode(); this.connectionLabel = connectionLabel(exch).orElse(null); this.initialRequest = initialRequest; - this.previousResponse = Optional.ofNullable(previousResponse); + this.previousResponse = previousResponse; this.headers = response.headers(); //this.trailers = trailers; - this.sslSession = Optional.ofNullable(response.getSSLSession()); + this.sslSession = response.getSSLSession(); this.uri = response.request().uri(); this.version = response.version(); this.rawChannelProvider = RawChannelProvider.create(response, exch); @@ -96,7 +96,7 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { @Override public Optional> previousResponse() { - return previousResponse; + return Optional.ofNullable(previousResponse); } @Override @@ -111,7 +111,7 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { @Override public Optional sslSession() { - return sslSession; + return Optional.ofNullable(sslSession); } @Override diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java index 1fb96944afc..b2c15ac3bef 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,9 @@ package jdk.internal.net.http; import java.net.URI; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption; import java.time.Duration; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient.Version; @@ -43,6 +45,8 @@ final class ImmutableHttpRequest extends HttpRequest { private final boolean expectContinue; private final Optional timeout; private final Optional version; + // An alternative would be to have one field per supported option + private final Map, Object> options; /** Creates an ImmutableHttpRequest from the given builder. */ ImmutableHttpRequest(HttpRequestBuilderImpl builder) { @@ -53,6 +57,7 @@ final class ImmutableHttpRequest extends HttpRequest { this.expectContinue = builder.expectContinue(); this.timeout = Optional.ofNullable(builder.timeout()); this.version = Objects.requireNonNull(builder.version()); + this.options = Map.copyOf(builder.options()); } @Override @@ -78,8 +83,17 @@ final class ImmutableHttpRequest extends HttpRequest { @Override public Optional version() { return version; } + @Override + public Optional getOption(HttpOption option) { + return Optional.ofNullable(option.type().cast(options.get(option))); + } + @Override public String toString() { return uri.toString() + " " + method; } + + public Map, Object> options() { + return options; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java index 20120aad7d5..ec621f7f955 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java @@ -29,7 +29,9 @@ import java.io.IOError; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.ConnectException; +import java.net.http.HttpClient.Version; import java.net.http.HttpConnectTimeoutException; +import java.net.http.StreamLimitException; import java.time.Duration; import java.util.List; import java.util.ListIterator; @@ -38,8 +40,6 @@ import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; @@ -62,6 +62,8 @@ import jdk.internal.net.http.common.ConnectionExpiredException; import jdk.internal.net.http.common.Utils; import static jdk.internal.net.http.common.MinimalFuture.completedFuture; import static jdk.internal.net.http.common.MinimalFuture.failedFuture; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcHeader; + /** * Encapsulates multiple Exchanges belonging to one HttpRequestImpl. @@ -76,6 +78,16 @@ class MultiExchange implements Cancelable { static final Logger debug = Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG); + private record RetryContext(Throwable requestFailureCause, + boolean shouldRetry, + AtomicInteger reqAttemptCounter, + boolean shouldResetConnectTimer) { + private static RetryContext doNotRetry(Throwable requestFailureCause) { + return new RetryContext(requestFailureCause, false, null, false); + } + } + + private static final AtomicLong IDS = new AtomicLong(); private final HttpRequest userRequest; // the user request private final HttpRequestImpl request; // a copy of the user request private final ConnectTimeoutTracker connectTimeout; // null if no timeout @@ -83,12 +95,11 @@ class MultiExchange implements Cancelable { final HttpResponse.BodyHandler responseHandler; final HttpClientImpl.DelegatingExecutor executor; final AtomicInteger attempts = new AtomicInteger(); + final long id = IDS.incrementAndGet(); HttpRequestImpl currentreq; // used for retries & redirect HttpRequestImpl previousreq; // used for retries & redirect Exchange exchange; // the current exchange Exchange previous; - volatile Throwable retryCause; - volatile boolean retriedOnce; volatile HttpResponse response; // Maximum number of times a request will be retried/redirected @@ -98,6 +109,12 @@ class MultiExchange implements Cancelable { "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS ); + // Maximum number of times a request should be retried when + // max streams limit is reached + static final int max_stream_limit_attempts = Utils.getIntegerNetProperty( + "jdk.httpclient.retryOnStreamlimit", max_attempts + ); + private final List filters; volatile ResponseTimerEvent responseTimerEvent; volatile boolean cancelled; @@ -113,20 +130,22 @@ class MultiExchange implements Cancelable { volatile AuthenticationFilter.AuthInfo serverauth, proxyauth; // RedirectHandler volatile int numberOfRedirects = 0; + // StreamLimit + private final AtomicInteger streamLimitRetries = new AtomicInteger(); // This class is used to keep track of the connection timeout // across retries, when a ConnectException causes a retry. // In that case - we will retry the connect, but we don't // want to double the timeout by starting a new timer with // the full connectTimeout again. - // Instead we use the ConnectTimeoutTracker to return a new + // Instead, we use the ConnectTimeoutTracker to return a new // duration that takes into account the time spent in the // first connect attempt. // If however, the connection gets connected, but we later // retry the whole operation, then we reset the timer before // retrying (since the connection used for the second request // will not necessarily be the same: it could be a new - // unconnected connection) - see getExceptionalCF(). + // unconnected connection) - see checkRetryEligible(). private static final class ConnectTimeoutTracker { final Duration max; final AtomicLong startTime = new AtomicLong(); @@ -199,8 +218,22 @@ class MultiExchange implements Cancelable { HttpClient.Version version() { HttpClient.Version vers = request.version().orElse(client.version()); - if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null) + if (vers != Version.HTTP_1_1 + && !request.secure() && request.proxy() != null + && !request.isHttp3Only(vers)) { + // downgrade to HTTP_1_1 unless HTTP_3_URI_ONLY. + // if HTTP_3_URI_ONLY and not secure it will fail down the road, so + // we don't downgrade here. vers = HttpClient.Version.HTTP_1_1; + } + if (vers == Version.HTTP_3 && request.secure() && !client.client3().isPresent()) { + if (!request.isHttp3Only(vers)) { + // HTTP/3 not supported with the client config. + // Downgrade to HTTP/2, unless HTTP_3_URI_ONLY is specified + vers = Version.HTTP_2; + if (debug.on()) debug.log("HTTP_3 downgraded to " + vers); + } + } return vers; } @@ -229,28 +262,28 @@ class MultiExchange implements Cancelable { } private void requestFilters(HttpRequestImpl r) throws IOException { - Log.logTrace("Applying request filters"); + if (Log.trace()) Log.logTrace("Applying request filters"); for (HeaderFilter filter : filters) { - Log.logTrace("Applying {0}", filter); + if (Log.trace()) Log.logTrace("Applying {0}", filter); filter.request(r, this); } - Log.logTrace("All filters applied"); + if (Log.trace()) Log.logTrace("All filters applied"); } private HttpRequestImpl responseFilters(Response response) throws IOException { - Log.logTrace("Applying response filters"); + if (Log.trace()) Log.logTrace("Applying response filters"); ListIterator reverseItr = filters.listIterator(filters.size()); while (reverseItr.hasPrevious()) { HeaderFilter filter = reverseItr.previous(); - Log.logTrace("Applying {0}", filter); + if (Log.trace()) Log.logTrace("Applying {0}", filter); HttpRequestImpl newreq = filter.response(response); if (newreq != null) { - Log.logTrace("New request: stopping filters"); + if (Log.trace()) Log.logTrace("New request: stopping filters"); return newreq; } } - Log.logTrace("All filters applied"); + if (Log.trace()) Log.logTrace("All filters applied"); return null; } @@ -293,9 +326,13 @@ class MultiExchange implements Cancelable { return true; } else { if (cancelled) { - debug.log("multi exchange already cancelled: " + interrupted.get()); + if (debug.on()) { + debug.log("multi exchange already cancelled: " + interrupted.get()); + } } else { - debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning); + if (debug.on()) { + debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning); + } } } return false; @@ -316,7 +353,7 @@ class MultiExchange implements Cancelable { // and therefore doesn't have to include header information which indicates no // body is present. This is distinct from responses that also do not contain // response bodies (possibly ever) but which are required to have content length - // info in the header (eg 205). Those cases do not have to be handled specially + // info in the header (e.g. 205). Those cases do not have to be handled specially private static boolean bodyNotPermitted(Response r) { return r.statusCode == 204; @@ -344,19 +381,27 @@ class MultiExchange implements Cancelable { if (exception != null) result.completeExceptionally(exception); else { - this.response = - new HttpResponseImpl<>(r.request(), r, this.response, nullBody, exch); - result.complete(this.response); + result.complete(setNewResponse(r.request(), r, nullBody, exch)); } }); // ensure that the connection is closed or returned to the pool. return result.whenComplete(exch::nullBody); } + // creates a new HttpResponseImpl object and assign it to this.response + private HttpResponse setNewResponse(HttpRequest request, Response r, T body, Exchange exch) { + HttpResponse previousResponse = this.response; + return this.response = new HttpResponseImpl<>(request, r, previousResponse, body, exch); + } + private CompletableFuture> responseAsync0(CompletableFuture start) { - return start.thenCompose( v -> responseAsyncImpl()) - .thenCompose((Response r) -> { + return start.thenCompose( _ -> { + // this is the first attempt to have the request processed by the server + attempts.set(1); + return responseAsyncImpl(true); + }).thenCompose((Response r) -> { + processAltSvcHeader(r, client(), currentreq); Exchange exch = getExchange(); if (bodyNotPermitted(r)) { if (bodyIsPresent(r)) { @@ -368,15 +413,11 @@ class MultiExchange implements Cancelable { return handleNoBody(r, exch); } return exch.readBodyAsync(responseHandler) - .thenApply((T body) -> { - this.response = - new HttpResponseImpl<>(r.request(), r, this.response, body, exch); - return this.response; - }); + .thenApply((T body) -> setNewResponse(r.request, r, body, exch)); }).exceptionallyCompose(this::whenCancelled); } - // returns a CancellationExcpetion that wraps the given cause + // returns a CancellationException that wraps the given cause // if cancel(boolean) was called, the given cause otherwise private Throwable wrapIfCancelled(Throwable cause) { CancellationException interrupt = interrupted.get(); @@ -412,79 +453,100 @@ class MultiExchange implements Cancelable { } } - private CompletableFuture responseAsyncImpl() { - CompletableFuture cf; - if (attempts.incrementAndGet() > max_attempts) { - cf = failedFuture(new IOException("Too many retries", retryCause)); - } else { - if (currentreq.timeout().isPresent()) { - responseTimerEvent = ResponseTimerEvent.of(this); - client.registerTimer(responseTimerEvent); - } - try { - // 1. apply request filters - // if currentreq == previousreq the filters have already - // been applied once. Applying them a second time might - // cause some headers values to be added twice: for - // instance, the same cookie might be added again. - if (currentreq != previousreq) { - requestFilters(currentreq); - } - } catch (IOException e) { - return failedFuture(e); - } - Exchange exch = getExchange(); - // 2. get response - cf = exch.responseAsync() - .thenCompose((Response response) -> { - HttpRequestImpl newrequest; - try { - // 3. apply response filters - newrequest = responseFilters(response); - } catch (Throwable t) { - IOException e = t instanceof IOException io ? io : new IOException(t); - exch.exchImpl.cancel(e); - return failedFuture(e); - } - // 4. check filter result and repeat or continue - if (newrequest == null) { - if (attempts.get() > 1) { - Log.logError("Succeeded on attempt: " + attempts); - } - return completedFuture(response); - } else { - cancelTimer(); - this.response = - new HttpResponseImpl<>(currentreq, response, this.response, null, exch); - Exchange oldExch = exch; - if (currentreq.isWebSocket()) { - // need to close the connection and open a new one. - exch.exchImpl.connection().close(); - } - return exch.ignoreBody().handle((r,t) -> { - previousreq = currentreq; - currentreq = newrequest; - retriedOnce = false; - setExchange(new Exchange<>(currentreq, this)); - return responseAsyncImpl(); - }).thenCompose(Function.identity()); - } }) - .handle((response, ex) -> { - // 5. handle errors and cancel any timer set - cancelTimer(); - if (ex == null) { - assert response != null; - return completedFuture(response); - } - // all exceptions thrown are handled here - CompletableFuture errorCF = getExceptionalCF(ex, exch.exchImpl); - if (errorCF == null) { - return responseAsyncImpl(); - } else { - return errorCF; - } }) - .thenCompose(Function.identity()); + // we call this only when a request is being retried + private CompletableFuture retryRequest() { + // maintain state indicating a request being retried + previousreq = currentreq; + // request is being retried, so the filters have already + // been applied once. Applying them a second time might + // cause some headers values to be added twice: for + // instance, the same cookie might be added again. + final boolean applyReqFilters = false; + return responseAsyncImpl(applyReqFilters); + } + + private CompletableFuture responseAsyncImpl(final boolean applyReqFilters) { + if (currentreq.timeout().isPresent()) { + responseTimerEvent = ResponseTimerEvent.of(this); + client.registerTimer(responseTimerEvent); } + try { + // 1. apply request filters + if (applyReqFilters) { + requestFilters(currentreq); + } + } catch (IOException e) { + return failedFuture(e); + } + final Exchange exch = getExchange(); + // 2. get response + final CompletableFuture cf = exch.responseAsync() + .thenCompose((Response response) -> { + HttpRequestImpl newrequest; + try { + // 3. apply response filters + newrequest = responseFilters(response); + } catch (Throwable t) { + IOException e = t instanceof IOException io ? io : new IOException(t); + exch.exchImpl.cancel(e); + return failedFuture(e); + } + // 4. check filter result and repeat or continue + if (newrequest == null) { + if (attempts.get() > 1) { + if (Log.requests()) { + Log.logResponse(() -> String.format( + "%s #%s Succeeded on attempt %s: statusCode=%s", + request, id, attempts, response.statusCode)); + } + } + return completedFuture(response); + } else { + cancelTimer(); + setNewResponse(currentreq, response, null, exch); + if (currentreq.isWebSocket()) { + // need to close the connection and open a new one. + exch.exchImpl.connection().close(); + } + return exch.ignoreBody().handle((r,t) -> { + previousreq = currentreq; + currentreq = newrequest; + // this is the first attempt to have the new request + // processed by the server + attempts.set(1); + setExchange(new Exchange<>(currentreq, this)); + return responseAsyncImpl(true); + }).thenCompose(Function.identity()); + } }) + .handle((response, ex) -> { + // 5. handle errors and cancel any timer set + cancelTimer(); + if (ex == null) { + assert response != null; + return completedFuture(response); + } + // all exceptions thrown are handled here + final RetryContext retryCtx = checkRetryEligible(ex, exch); + assert retryCtx != null : "retry context is null"; + if (retryCtx.shouldRetry()) { + // increment the request attempt counter and retry the request + assert retryCtx.reqAttemptCounter != null : "request attempt counter is null"; + final int numAttempt = retryCtx.reqAttemptCounter.incrementAndGet(); + if (debug.on()) { + debug.log("Retrying request: " + currentreq + " id: " + id + + " attempt: " + numAttempt + " due to: " + + retryCtx.requestFailureCause); + } + // reset the connect timer if necessary + if (retryCtx.shouldResetConnectTimer && this.connectTimeout != null) { + this.connectTimeout.reset(); + } + return retryRequest(); + } else { + assert retryCtx.requestFailureCause != null : "missing request failure cause"; + return MinimalFuture.failedFuture(retryCtx.requestFailureCause); + } }) + .thenCompose(Function.identity()); return cf; } @@ -492,14 +554,14 @@ class MultiExchange implements Cancelable { String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry"); if (s == null) return false; - return s.isEmpty() ? true : Boolean.parseBoolean(s); + return s.isEmpty() || Boolean.parseBoolean(s); } private static boolean disableRetryConnect() { String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect"); if (s == null) return false; - return s.isEmpty() ? true : Boolean.parseBoolean(s); + return s.isEmpty() || Boolean.parseBoolean(s); } /** True if ALL ( even non-idempotent ) requests can be automatic retried. */ @@ -517,7 +579,7 @@ class MultiExchange implements Cancelable { } /** Returns true if the given request can be automatically retried. */ - private static boolean canRetryRequest(HttpRequest request) { + private static boolean isHttpMethodRetriable(HttpRequest request) { if (RETRY_ALWAYS) return true; if (isIdempotentRequest(request)) @@ -534,70 +596,125 @@ class MultiExchange implements Cancelable { return interrupted.get() != null; } - private boolean retryOnFailure(Throwable t) { - if (requestCancelled()) return false; - return t instanceof ConnectionExpiredException - || (RETRY_CONNECT && (t instanceof ConnectException)); - } - - private Throwable retryCause(Throwable t) { - Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t; - return cause == null ? t : cause; + String streamLimitState() { + return id + " attempt:" + streamLimitRetries.get(); } /** - * Takes a Throwable and returns a suitable CompletableFuture that is - * completed exceptionally, or null. + * This method determines if a failed request can be retried. The returned RetryContext + * will contain the {@linkplain RetryContext#shouldRetry() retry decision} and the + * {@linkplain RetryContext#requestFailureCause() underlying + * cause} (computed out of the given {@code requestFailureCause}) of the request failure. + * + * @param requestFailureCause the exception that caused the request to fail + * @param exchg the Exchange + * @return a non-null RetryContext which contains the result of retry eligibility */ - private CompletableFuture getExceptionalCF(Throwable t, ExchangeImpl exchImpl) { - if ((t instanceof CompletionException) || (t instanceof ExecutionException)) { - if (t.getCause() != null) { - t = t.getCause(); + private RetryContext checkRetryEligible(final Throwable requestFailureCause, + final Exchange exchg) { + assert requestFailureCause != null : "request failure cause is missing"; + assert exchg != null : "exchange cannot be null"; + // determine the underlying cause for the request failure + final Throwable t = Utils.getCompletionCause(requestFailureCause); + final Throwable underlyingCause = switch (t) { + case IOException ioe -> { + if (cancelled && !requestCancelled() && !(ioe instanceof HttpTimeoutException)) { + yield toTimeoutException(ioe); + } + yield ioe; } + default -> { + yield t; + } + }; + if (requestCancelled()) { + // request has been cancelled, do not retry + return RetryContext.doNotRetry(underlyingCause); } - final boolean retryAsUnprocessed = exchImpl != null && exchImpl.isUnprocessedByPeer(); - if (cancelled && !requestCancelled() && t instanceof IOException) { - if (!(t instanceof HttpTimeoutException)) { - t = toTimeoutException((IOException)t); + // check if retry limited is reached. if yes then don't retry. + record Limit(int numAttempts, int maxLimit) { + boolean retryLimitReached() { + return Limit.this.numAttempts >= Limit.this.maxLimit; } - } else if (retryAsUnprocessed || retryOnFailure(t)) { - Throwable cause = retryCause(t); - - if (!(t instanceof ConnectException)) { - // we may need to start a new connection, and if so - // we want to start with a fresh connect timeout again. - if (connectTimeout != null) connectTimeout.reset(); - if (!retryAsUnprocessed && !canRetryRequest(currentreq)) { - // a (peer) processed request which cannot be retried, fail with - // the original cause - return failedFuture(cause); - } - } // ConnectException: retry, but don't reset the connectTimeout. - - // allow the retry mechanism to do its work - retryCause = cause; - if (!retriedOnce) { - if (debug.on()) { - debug.log(t.getClass().getSimpleName() - + " (async): retrying " + currentreq + " due to: ", t); - } - retriedOnce = true; - // The connection was abruptly closed. - // We return null to retry the same request a second time. - // The request filters have already been applied to the - // currentreq, so we set previousreq = currentreq to - // prevent them from being applied again. - previousreq = currentreq; - return null; - } else { - if (debug.on()) { - debug.log(t.getClass().getSimpleName() - + " (async): already retried once " + currentreq, t); - } - t = cause; + }; + final Limit limit = switch (underlyingCause) { + case StreamLimitException _ -> { + yield new Limit(streamLimitRetries.get(), max_stream_limit_attempts); } + case ConnectException _ -> { + // for ConnectException (i.e. inability to establish a connection to the server) + // we currently retry the request only once and don't honour the + // "jdk.httpclient.redirects.retrylimit" configuration value. + yield new Limit(attempts.get(), 2); + } + default -> { + yield new Limit(attempts.get(), max_attempts); + } + }; + if (limit.retryLimitReached()) { + if (debug.on()) { + debug.log("request already attempted " + + limit.numAttempts + " times, won't be retried again " + + currentreq + " " + id, underlyingCause); + } + final var x = underlyingCause instanceof ConnectionExpiredException cee + ? cee.getCause() == null ? cee : cee.getCause() + : underlyingCause; + // do not retry anymore + return RetryContext.doNotRetry(x); } - return failedFuture(t); + return switch (underlyingCause) { + case ConnectException _ -> { + // connection attempt itself failed, so the request hasn't reached the server. + // check if retry on connection failure is enabled, if not then we don't retry + // the request. + if (!RETRY_CONNECT) { + // do not retry + yield RetryContext.doNotRetry(underlyingCause); + } + // OK to retry. Since the failure is due to a connection/stream being unavailable + // we mark the retry context to not allow the connect timer to be reset + // when the retry is actually attempted. + yield new RetryContext(underlyingCause, true, attempts, false); + } + case StreamLimitException sle -> { + // make a note that the stream limit was reached for a particular HTTP version + exchg.streamLimitReached(true); + // OK to retry. Since the failure is due to a connection/stream being unavailable + // we mark the retry context to not allow the connect timer to be reset + // when the retry is actually attempted. + yield new RetryContext(underlyingCause, true, streamLimitRetries, false); + } + case ConnectionExpiredException cee -> { + final Throwable cause = cee.getCause() == null ? cee : cee.getCause(); + // check if the request was explicitly marked as unprocessed, in which case + // we retry + if (exchg.isUnprocessedByPeer()) { + // OK to retry and allow for the connect timer to be reset + yield new RetryContext(cause, true, attempts, true); + } + // the request which failed hasn't been marked as unprocessed which implies that + // it could be processed by the server. check if the request's METHOD allows + // for retry. + if (!isHttpMethodRetriable(currentreq)) { + // request METHOD doesn't allow for retry + yield RetryContext.doNotRetry(cause); + } + // OK to retry and allow for the connect timer to be reset + yield new RetryContext(cause, true, attempts, true); + } + default -> { + // some other exception that caused the request to fail. + // we check if the request has been explicitly marked as "unprocessed", + // which implies the server hasn't processed the request and is thus OK to retry. + if (exchg.isUnprocessedByPeer()) { + // OK to retry and allow for resetting the connect timer + yield new RetryContext(underlyingCause, true, attempts, false); + } + // some other cause of failure, do not retry. + yield RetryContext.doNotRetry(underlyingCause); + } + }; } private HttpTimeoutException toTimeoutException(IOException ioe) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java index adbee565297..8aee8ef2230 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java @@ -26,6 +26,7 @@ package jdk.internal.net.http; import java.net.URI; +import java.net.URISyntaxException; import java.util.Locale; import java.util.Objects; @@ -132,6 +133,46 @@ public record Origin(String scheme, String host, int port) { return host + ":" + port; } + /** + * {@return true if the Origin's scheme is considered secure, else returns false} + */ + boolean isSecure() { + // we consider https to be the only secure scheme + return scheme.equals("https"); + } + + /** + * {@return Creates and returns an Origin parsed from the ASCII serialized form as defined + * in section 6.2 of RFC-6454} + * + * @param value The value to be parsed + */ + static Origin fromASCIISerializedForm(final String value) throws IllegalArgumentException { + Objects.requireNonNull(value); + try { + final URI uri = new URI(value); + // the ASCII-serialized form contains scheme://host, optionally followed by :port + if (uri.getScheme() == null || uri.getHost() == null) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin"); + } + // normalize the origin string, check if we get the same result + String normalized = uri.getScheme() + "://" + uri.getHost(); + if (uri.getPort() != -1) { + normalized += ":" + uri.getPort(); + } + if (!value.equals(normalized)) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin"); + } + try { + return Origin.from(uri); + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin", iae); + } + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin", use); + } + } + private static boolean isValidScheme(final String scheme) { // only "http" and "https" literals allowed return "http".equals(scheme) || "https".equals(scheme); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java index d0d64312f1f..e705aae72a1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java @@ -266,24 +266,8 @@ class PlainHttpConnection extends HttpConnection { try { this.chan = SocketChannel.open(); chan.configureBlocking(false); - if (debug.on()) { - int bufsize = getSoReceiveBufferSize(); - debug.log("Initial receive buffer size is: %d", bufsize); - bufsize = getSoSendBufferSize(); - debug.log("Initial send buffer size is: %d", bufsize); - } - if (trySetReceiveBufferSize(client.getReceiveBufferSize())) { - if (debug.on()) { - int bufsize = getSoReceiveBufferSize(); - debug.log("Receive buffer size configured: %d", bufsize); - } - } - if (trySetSendBufferSize(client.getSendBufferSize())) { - if (debug.on()) { - int bufsize = getSoSendBufferSize(); - debug.log("Send buffer size configured: %d", bufsize); - } - } + Utils.configureChannelBuffers(debug::log, chan, + client.getReceiveBufferSize(), client.getSendBufferSize()); chan.setOption(StandardSocketOptions.TCP_NODELAY, true); // wrap the channel in a Tube for async reading and writing tube = new SocketTube(client(), chan, Utils::getBuffer, label); @@ -292,54 +276,6 @@ class PlainHttpConnection extends HttpConnection { } } - private int getSoReceiveBufferSize() { - try { - return chan.getOption(StandardSocketOptions.SO_RCVBUF); - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to get initial receive buffer size on %s", chan); - } - return 0; - } - - private int getSoSendBufferSize() { - try { - return chan.getOption(StandardSocketOptions.SO_SNDBUF); - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to get initial receive buffer size on %s", chan); - } - return 0; - } - - private boolean trySetReceiveBufferSize(int bufsize) { - try { - if (bufsize > 0) { - chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize); - return true; - } - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to set receive buffer size to %d on %s", - bufsize, chan); - } - return false; - } - - private boolean trySetSendBufferSize(int bufsize) { - try { - if (bufsize > 0) { - chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize); - return true; - } - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to set send buffer size to %d on %s", - bufsize, chan); - } - return false; - } - @Override HttpPublisher publisher() { return writePublisher; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java index 0556214648e..d1019c05629 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,46 +25,55 @@ package jdk.internal.net.http; -import java.util.Iterator; import java.util.concurrent.Flow; + import jdk.internal.net.http.common.Demand; import jdk.internal.net.http.common.SequentialScheduler; /** - * A Publisher that publishes items obtained from the given Iterable. Each new - * subscription gets a new Iterator. + * A {@linkplain Flow.Publisher publisher} that publishes items obtained from the given {@link CheckedIterable}. + * Each new subscription gets a new {@link CheckedIterator}. */ class PullPublisher implements Flow.Publisher { - // Only one of `iterable` and `throwable` can be non-null. throwable is + // Only one of `iterable` or `throwable` should be null, and the other non-null. throwable is // non-null when an error has been encountered, by the creator of // PullPublisher, while subscribing the subscriber, but before subscribe has // completed. - private final Iterable iterable; + private final CheckedIterable iterable; private final Throwable throwable; - PullPublisher(Iterable iterable, Throwable throwable) { + PullPublisher(CheckedIterable iterable, Throwable throwable) { + if ((iterable == null) == (throwable == null)) { + String message = String.format( + "only one of `iterable` or `throwable` should be null, and the other non-null, but %s are null", + throwable == null ? "both" : "none"); + throw new IllegalArgumentException(message); + } this.iterable = iterable; this.throwable = throwable; } - PullPublisher(Iterable iterable) { + PullPublisher(CheckedIterable iterable) { this(iterable, null); } @Override public void subscribe(Flow.Subscriber subscriber) { - Subscription sub; - if (throwable != null) { - assert iterable == null : "non-null iterable: " + iterable; - sub = new Subscription(subscriber, null, throwable); - } else { - assert throwable == null : "non-null exception: " + throwable; - sub = new Subscription(subscriber, iterable.iterator(), null); + Throwable failure = throwable; + CheckedIterator iterator = null; + if (failure == null) { + try { + iterator = iterable.iterator(); + } catch (Exception exception) { + failure = exception; + } } + Subscription sub = failure != null + ? new Subscription(subscriber, null, failure) + : new Subscription(subscriber, iterator, null); subscriber.onSubscribe(sub); - - if (throwable != null) { + if (failure != null) { sub.pullScheduler.runOrSchedule(); } } @@ -72,7 +81,7 @@ class PullPublisher implements Flow.Publisher { private class Subscription implements Flow.Subscription { private final Flow.Subscriber subscriber; - private final Iterator iter; + private final CheckedIterator iter; private volatile boolean completed; private volatile boolean cancelled; private volatile Throwable error; @@ -80,7 +89,7 @@ class PullPublisher implements Flow.Publisher { private final Demand demand = new Demand(); Subscription(Flow.Subscriber subscriber, - Iterator iter, + CheckedIterator iter, Throwable throwable) { this.subscriber = subscriber; this.iter = iter; @@ -117,7 +126,18 @@ class PullPublisher implements Flow.Publisher { } subscriber.onNext(next); } - if (!iter.hasNext() && !cancelled) { + + boolean hasNext; + try { + hasNext = iter.hasNext(); + } catch (Exception e) { + completed = true; + pullScheduler.stop(); + subscriber.onError(e); + return; + } + + if (!hasNext && !cancelled) { completed = true; pullScheduler.stop(); subscriber.onComplete(); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java index f2c7a61c9b6..6bf03f195a7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java @@ -25,6 +25,7 @@ package jdk.internal.net.http; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.net.http.HttpRequest; @@ -105,9 +106,21 @@ class PushGroup { } Acceptor acceptPushRequest(HttpRequest pushRequest) { + return doAcceptPushRequest(pushRequest, null); + } + + Acceptor acceptPushRequest(HttpRequest pushRequest, PushId pushId) { + return doAcceptPushRequest(pushRequest, Objects.requireNonNull(pushId)); + } + + private Acceptor doAcceptPushRequest(HttpRequest pushRequest, PushId pushId) { AcceptorImpl acceptor = new AcceptorImpl<>(executor); try { - pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept); + if (pushId == null) { + pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept); + } else { + pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, pushId, acceptor::accept); + } } catch (Throwable t) { if (acceptor.accepted()) { CompletableFuture cf = acceptor.cf(); @@ -128,6 +141,10 @@ class PushGroup { } } + void acceptPushPromiseId(PushId pushId) { + pushPromiseHandler.notifyAdditionalPromise(initiatingRequest, pushId); + } + void pushCompleted() { stateLock.lock(); try { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java index 88cabe15419..5aea5648e19 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java @@ -73,8 +73,11 @@ public final class RequestPublishers { this(content, offset, length, Utils.BUFSIZE); } - /* bufSize exposed for testing purposes */ - ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { + private ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { + Objects.checkFromIndexSize(offset, length, content.length); // Implicit null check on `content` + if (bufSize <= 0) { + throw new IllegalArgumentException("Invalid buffer size: " + bufSize); + } this.content = content; this.offset = offset; this.length = length; @@ -99,7 +102,7 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { List copy = copy(content, offset, length); - var delegate = new PullPublisher<>(copy); + var delegate = new PullPublisher<>(CheckedIterable.fromIterable(copy)); delegate.subscribe(subscriber); } @@ -121,7 +124,7 @@ public final class RequestPublishers { // The ByteBufferIterator will iterate over the byte[] arrays in // the content one at the time. // - class ByteBufferIterator implements Iterator { + private final class ByteBufferIterator implements CheckedIterator { final ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); final Iterator iterator = content.iterator(); @Override @@ -166,13 +169,9 @@ public final class RequestPublishers { } } - public Iterator iterator() { - return new ByteBufferIterator(); - } - @Override public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = this::iterator; + CheckedIterable iterable = () -> new ByteBufferIterator(); var delegate = new PullPublisher<>(iterable); delegate.subscribe(subscriber); } @@ -202,13 +201,13 @@ public final class RequestPublishers { public static class StringPublisher extends ByteArrayPublisher { public StringPublisher(String content, Charset charset) { - super(content.getBytes(charset)); + super(content.getBytes(Objects.requireNonNull(charset))); // Implicit null check on `content` } } public static class EmptyPublisher implements BodyPublisher { private final Flow.Publisher delegate = - new PullPublisher(Collections.emptyList(), null); + new PullPublisher<>(CheckedIterable.fromIterable(Collections.emptyList()), null); @Override public long contentLength() { @@ -290,7 +289,7 @@ public final class RequestPublishers { /** * Reads one buffer ahead all the time, blocking in hasNext() */ - public static class StreamIterator implements Iterator { + private static final class StreamIterator implements CheckedIterator { final InputStream is; final Supplier bufSupplier; private volatile boolean eof; @@ -331,20 +330,8 @@ public final class RequestPublishers { return n; } - /** - * Close stream in this instance. - * UncheckedIOException may be thrown if IOE happens at InputStream::close. - */ - private void closeStream() { - try { - is.close(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - @Override - public boolean hasNext() { + public boolean hasNext() throws IOException { stateLock.lock(); try { return hasNext0(); @@ -353,7 +340,7 @@ public final class RequestPublishers { } } - private boolean hasNext0() { + private boolean hasNext0() throws IOException { if (need2Read) { try { haveNext = read() != -1; @@ -363,10 +350,10 @@ public final class RequestPublishers { } catch (IOException e) { haveNext = false; need2Read = false; - throw new UncheckedIOException(e); + throw e; } finally { if (!haveNext) { - closeStream(); + is.close(); } } } @@ -374,7 +361,7 @@ public final class RequestPublishers { } @Override - public ByteBuffer next() { + public ByteBuffer next() throws IOException { stateLock.lock(); try { if (!hasNext()) { @@ -398,18 +385,23 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { - PullPublisher publisher; - InputStream is = streamSupplier.get(); - if (is == null) { - Throwable t = new IOException("streamSupplier returned null"); - publisher = new PullPublisher<>(null, t); - } else { - publisher = new PullPublisher<>(iterableOf(is), null); + InputStream is = null; + Exception exception = null; + try { + is = streamSupplier.get(); + if (is == null) { + exception = new IOException("Stream supplier returned null"); + } + } catch (Exception cause) { + exception = new IOException("Stream supplier has failed", cause); } + PullPublisher publisher = exception != null + ? new PullPublisher<>(null, exception) + : new PullPublisher<>(iterableOf(is), null); publisher.subscribe(subscriber); } - protected Iterable iterableOf(InputStream is) { + private CheckedIterable iterableOf(InputStream is) { return () -> new StreamIterator(is); } @@ -442,13 +434,13 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = () -> new FileChannelIterator(channel, position, limit); + CheckedIterable iterable = () -> new FileChannelIterator(channel, position, limit); new PullPublisher<>(iterable).subscribe(subscriber); } } - private static final class FileChannelIterator implements Iterator { + private static final class FileChannelIterator implements CheckedIterator { private final FileChannel channel; @@ -470,7 +462,7 @@ public final class RequestPublishers { } @Override - public ByteBuffer next() { + public ByteBuffer next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } @@ -487,7 +479,7 @@ public final class RequestPublishers { } } catch (IOException ioe) { terminated = true; - throw new UncheckedIOException(ioe); + throw ioe; } return buffer.flip(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java index 949024453ca..e416773ee61 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,14 @@ package jdk.internal.net.http; -import java.net.URI; import java.io.IOException; +import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.InetSocketAddress; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; + import jdk.internal.net.http.common.Utils; /** @@ -71,17 +72,14 @@ class Response { this.statusCode = statusCode; this.isConnectResponse = isConnectResponse; if (connection != null) { - InetSocketAddress a; - try { - a = (InetSocketAddress)connection.channel().getLocalAddress(); - } catch (IOException e) { - a = null; - } - this.localAddress = a; - if (connection instanceof AbstractAsyncSSLConnection) { - AbstractAsyncSSLConnection cc = (AbstractAsyncSSLConnection)connection; + this.localAddress = revealedLocalSocketAddress(connection); + if (connection instanceof AbstractAsyncSSLConnection cc) { SSLEngine engine = cc.getEngine(); sslSession = Utils.immutableSession(engine.getSession()); + } else if (connection instanceof HttpQuicConnection qc) { + // TODO: consider adding Optional getSession() to HttpConnection? + var session = qc.quicConnection().getTLSEngine().getSession(); + sslSession = Utils.immutableSession(session); } else { sslSession = null; } @@ -128,4 +126,12 @@ class Response { sb.append(" Local port: ").append(localAddress.getPort()); return sb.toString(); } + + private static InetSocketAddress revealedLocalSocketAddress(HttpConnection connection) { + try { + return (InetSocketAddress) connection.channel().getLocalAddress(); + } catch (IOException io) { + return null; + } + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java index 04d019e4c81..071c68720ac 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java @@ -526,7 +526,7 @@ public class ResponseSubscribers { @Override public void onError(Throwable thrwbl) { if (debug.on()) - debug.log("onError called: " + thrwbl); + debug.log("onError called", thrwbl); subscription = null; failed = Objects.requireNonNull(thrwbl); // The client process that reads the input stream might @@ -1086,6 +1086,16 @@ public class ResponseSubscribers { bs.getBody().whenComplete((r, t) -> { if (t != null) { cf.completeExceptionally(t); + // if a user-provided BodySubscriber returns + // a getBody() CF completed exceptionally, it's + // the responsibility of that BodySubscriber to cancel + // its subscription in order to cancel the request, + // if operations are still in progress. + // Calling the errorHandler here would ensure that the + // request gets cancelled, but there me cases where that is + // not what the caller wants. Therefore, it's better to + // not call `errorHandler.accept(t);` here, but leave it + // to the provided BodySubscriber implementation. } else { cf.complete(r); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java index b5dada882b2..4b59a777de2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java @@ -59,6 +59,8 @@ import jdk.internal.net.http.common.*; import jdk.internal.net.http.frame.*; import jdk.internal.net.http.hpack.DecodingCallback; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame; + import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; /** @@ -96,8 +98,8 @@ import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; * placed on the stream's inputQ which is consumed by the stream's * reader thread. * - * PushedStream sub class - * ====================== + * PushedStream subclass + * ===================== * Sending side methods are not used because the request comes from a PUSH_PROMISE * frame sent by the server. When a PUSH_PROMISE is received the PushedStream * is created. PushedStream does not use responseCF list as there can be only @@ -151,7 +153,7 @@ class Stream extends ExchangeImpl { // Indicates the first reason that was invoked when sending a ResetFrame // to the server. A streamState of 0 indicates that no reset was sent. // (see markStream(int code) - private volatile int streamState; // assigned using STREAM_STATE varhandle. + private volatile int streamState; // assigned while holding the sendLock. private volatile boolean deRegistered; // assigned using DEREGISTERED varhandle. // state flags @@ -219,7 +221,7 @@ class Stream extends ExchangeImpl { List buffers = df.getData(); List dsts = Collections.unmodifiableList(buffers); - int size = Utils.remaining(dsts, Integer.MAX_VALUE); + long size = Utils.remaining(dsts, Long.MAX_VALUE); if (size == 0 && finished) { inputQ.remove(); // consumed will not be called @@ -478,7 +480,9 @@ class Stream extends ExchangeImpl { if (code == 0) return streamState; sendLock.lock(); try { - return (int) STREAM_STATE.compareAndExchange(this, 0, code); + var state = streamState; + if (state == 0) streamState = code; + return state; } finally { sendLock.unlock(); } @@ -534,7 +538,7 @@ class Stream extends ExchangeImpl { this.requestPublisher = request.requestPublisher; // may be null this.responseHeadersBuilder = new HttpHeadersBuilder(); this.rspHeadersConsumer = new HeadersConsumer(); - this.requestPseudoHeaders = createPseudoHeaders(request); + this.requestPseudoHeaders = Utils.createPseudoHeaders(request); this.streamWindowUpdater = new StreamWindowUpdateSender(connection); } @@ -587,6 +591,7 @@ class Stream extends ExchangeImpl { case WindowUpdateFrame.TYPE -> incoming_windowUpdate((WindowUpdateFrame) frame); case ResetFrame.TYPE -> incoming_reset((ResetFrame) frame); case PriorityFrame.TYPE -> incoming_priority((PriorityFrame) frame); + case AltSvcFrame.TYPE -> handleAltSvcFrame(streamid, (AltSvcFrame) frame); default -> throw new IOException("Unexpected frame: " + frame); } @@ -745,6 +750,10 @@ class Stream extends ExchangeImpl { } } + void handleAltSvcFrame(int streamid, AltSvcFrame asf) { + processAltSvcFrame(streamid, asf, connection.connection, connection.client()); + } + void handleReset(ResetFrame frame, Flow.Subscriber subscriber) { Log.logTrace("Handling RST_STREAM on stream {0}", streamid); if (!closed) { @@ -763,12 +772,16 @@ class Stream extends ExchangeImpl { // A REFUSED_STREAM error code implies that the stream wasn't processed by the // peer and the client is free to retry the request afresh. if (error == ErrorFrame.REFUSED_STREAM) { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be marked as unprocessed"; // Here we arrange for the request to be retried. Note that we don't call // closeAsUnprocessed() method here because the "closed" state is already set // to true a few lines above and calling close() from within // closeAsUnprocessed() will end up being a no-op. We instead do the additional // bookkeeping here. - markUnprocessedByPeer(); + this.exchange.markUnprocessedByPeer(); errorRef.compareAndSet(null, new IOException("request not processed by peer")); if (debug.on()) { debug.log("request unprocessed by peer (REFUSED_STREAM) " + this.request); @@ -1216,6 +1229,7 @@ class Stream extends ExchangeImpl { assert !endStreamSent : "internal error, send data after END_STREAM flag"; } if ((state = streamState) != 0) { + t = errorRef.get(); if (debug.on()) debug.log("trySend: cancelled: %s", String.valueOf(t)); break; } @@ -1521,7 +1535,7 @@ class Stream extends ExchangeImpl { } else cancelImpl(cause); } - // This method sends a RST_STREAM frame + // This method sends an RST_STREAM frame void cancelImpl(Throwable e) { cancelImpl(e, ResetFrame.CANCEL); } @@ -1856,8 +1870,12 @@ class Stream extends ExchangeImpl { */ void closeAsUnprocessed() { try { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed"; // We arrange for the request to be retried on a new connection as allowed by the RFC-9113 - markUnprocessedByPeer(); + this.exchange.markUnprocessedByPeer(); this.errorRef.compareAndSet(null, new IOException("request not processed by peer")); if (debug.on()) { debug.log("closing " + this.request + " as unprocessed by peer"); @@ -1905,7 +1923,7 @@ class Stream extends ExchangeImpl { streamid, n, v); } } catch (UncheckedIOException uio) { - // reset stream: From RFC 9113, section 8.1 + // reset stream: From RFC 7540, section-8.1.2.6 // Malformed requests or responses that are detected MUST be // treated as a stream error (Section 5.4.2) of type // PROTOCOL_ERROR. @@ -1953,13 +1971,10 @@ class Stream extends ExchangeImpl { } - private static final VarHandle STREAM_STATE; private static final VarHandle DEREGISTERED; static { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); - STREAM_STATE = lookup - .findVarHandle(Stream.class, "streamState", int.class); DEREGISTERED = lookup .findVarHandle(Stream.class, "deRegistered", boolean.class); } catch (Exception x) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java index 66397d93410..5888e6c2de3 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java @@ -34,4 +34,9 @@ public final class Alpns { public static final String HTTP_1_1 = "http/1.1"; public static final String H2 = "h2"; public static final String H2C = "h2c"; + public static final String H3 = "h3"; + + public static boolean isSecureALPNName(final String alpnName) { + return H3.equals(alpnName) || H2.equals(alpnName); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java index 07091886533..164e1e5685d 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java @@ -29,7 +29,9 @@ import java.io.IOException; /** * Signals that an end of file or end of stream has been reached - * unexpectedly before any protocol specific data has been received. + * unexpectedly before any protocol specific data has been received, + * or that a new stream creation was rejected because the underlying + * connection was closed. */ public final class ConnectionExpiredException extends IOException { private static final long serialVersionUID = 0; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java index bc9a992b3bd..3ee334885a3 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java @@ -88,6 +88,24 @@ public final class Deadline implements Comparable { return of(deadline.truncatedTo(unit)); } + /** + * Returns a copy of this deadline with the specified amount subtracted. + *

    + * This returns a {@code Deadline}, based on this one, with the specified amount subtracted. + * The amount is typically {@link Duration} but may be any other type implementing + * the {@link TemporalAmount} interface. + *

    + * This instance is immutable and unaffected by this method call. + * + * @param amountToSubtract the amount to subtract, not null + * @return a {@code Deadline} based on this deadline with the subtraction made, not null + * @throws DateTimeException if the subtraction cannot be made + * @throws ArithmeticException if numeric overflow occurs + */ + public Deadline minus(TemporalAmount amountToSubtract) { + return Deadline.of(deadline.minus(amountToSubtract)); + } + /** * Returns a copy of this deadline with the specified amount added. *

    @@ -126,6 +144,21 @@ public final class Deadline implements Comparable { return Deadline.of(deadline.plusSeconds(secondsToAdd)); } + /** + * Returns a copy of this deadline with the specified duration in milliseconds added. + *

    + * This instance is immutable and unaffected by this method call. + * + * @param millisToAdd the milliseconds to add, positive or negative + * @return a {@code Deadline} based on this deadline with the specified milliseconds added, not null + * @throws DateTimeException if the result exceeds the maximum or minimum deadline + * @throws ArithmeticException if numeric overflow occurs + */ + public Deadline plusMillis(long millisToAdd) { + if (millisToAdd == 0) return this; + return Deadline.of(deadline.plusMillis(millisToAdd)); + } + /** * Returns a copy of this deadline with the specified amount added. *

    @@ -183,7 +216,7 @@ public final class Deadline implements Comparable { /** * Checks if this deadline is before the specified deadline. *

    - * The comparison is based on the time-line position of the deadines. + * The comparison is based on the time-line position of the deadlines. * * @param otherDeadline the other deadine to compare to, not null * @return true if this deadline is before the specified deadine @@ -217,7 +250,26 @@ public final class Deadline implements Comparable { return deadline.hashCode(); } + Instant asInstant() { + return deadline; + } + static Deadline of(Instant instant) { return new Deadline(instant); } + + /** + * Obtains a {@code Duration} representing the duration between two deadlines. + *

    + * The result of this method can be a negative period if the end is before the start. + * + * @param startInclusive the start deadline, inclusive, not null + * @param endExclusive the end deadline, exclusive, not null + * @return a {@code Duration}, not null + * @throws DateTimeException if the seconds between the deadline cannot be obtained + * @throws ArithmeticException if the calculation exceeds the capacity of {@code Duration} + */ + public static Duration between(Deadline startInclusive, Deadline endExclusive) { + return Duration.between(startInclusive.deadline, endExclusive.deadline); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java index 6dc79760b0a..1c483ce99f4 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java @@ -284,6 +284,7 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber { */ public final void complete(Throwable t) { if (markCompleted()) { + logComplete(t); tryUnregister(); t = withError = Utils.getCompletionCause(t); if (t == null) { @@ -312,6 +313,10 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber { } } + protected void logComplete(Throwable error) { + + } + /** * {@return true if this subscriber has already completed, either normally * or abnormally} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java index 409a8540b68..7c1d2311ba9 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,15 @@ public class HttpHeadersBuilder { headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } + // used in test library (Http3ServerExchange) + public HttpHeadersBuilder(HttpHeaders headers) { + headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry> entry : headers.map().entrySet()) { + List valuesCopy = new ArrayList<>(entry.getValue()); + headersMap.put(entry.getKey(), valuesCopy); + } + } + public HttpHeadersBuilder structuralCopy() { HttpHeadersBuilder builder = new HttpHeadersBuilder(); for (Map.Entry> entry : headersMap.entrySet()) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java index 48f5a2b06d8..bc89a6e9d8e 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,14 +28,28 @@ package jdk.internal.net.http.common; import java.net.http.HttpHeaders; import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Stream; + import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.Http2Frame; import jdk.internal.net.http.frame.WindowUpdateFrame; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLParameters; @@ -43,7 +57,8 @@ import javax.net.ssl.SSLParameters; /** * -Djdk.httpclient.HttpClient.log= * errors,requests,headers, - * frames[:control:data:window:all..],content,ssl,trace,channel + * frames[:control:data:window:all..],content,ssl,trace,channel, + * quic[:control:processed:retransmit:ack:crypto:data:cc:hs:dbb:ping:all] * * Any of errors, requests, headers or content are optional. * @@ -57,15 +72,17 @@ public abstract class Log implements System.Logger { static final String logProp = "jdk.httpclient.HttpClient.log"; - public static final int OFF = 0; - public static final int ERRORS = 0x1; - public static final int REQUESTS = 0x2; - public static final int HEADERS = 0x4; - public static final int CONTENT = 0x8; - public static final int FRAMES = 0x10; - public static final int SSL = 0x20; - public static final int TRACE = 0x40; - public static final int CHANNEL = 0x80; + public static final int OFF = 0x00; + public static final int ERRORS = 0x01; + public static final int REQUESTS = 0x02; + public static final int HEADERS = 0x04; + public static final int CONTENT = 0x08; + public static final int FRAMES = 0x10; + public static final int SSL = 0x20; + public static final int TRACE = 0x40; + public static final int CHANNEL = 0x80; + public static final int QUIC = 0x0100; + public static final int HTTP3 = 0x0200; static int logging; // Frame types: "control", "data", "window", "all" @@ -75,6 +92,27 @@ public abstract class Log implements System.Logger { public static final int ALL = CONTROL| DATA | WINDOW_UPDATES; static int frametypes; + // Quic message types + public static final int QUIC_CONTROL = 1; + public static final int QUIC_PROCESSED = 2; + public static final int QUIC_RETRANSMIT = 4; + public static final int QUIC_DATA = 8; + public static final int QUIC_CRYPTO = 16; + public static final int QUIC_ACK = 32; + public static final int QUIC_PING = 64; + public static final int QUIC_CC = 128; + public static final int QUIC_TIMER = 256; + public static final int QUIC_DIRECT_BUFFER_POOL = 512; + public static final int QUIC_HANDSHAKE = 1024; + public static final int QUIC_ALL = QUIC_CONTROL + | QUIC_PROCESSED | QUIC_RETRANSMIT + | QUIC_DATA | QUIC_CRYPTO + | QUIC_ACK | QUIC_PING | QUIC_CC + | QUIC_TIMER | QUIC_DIRECT_BUFFER_POOL + | QUIC_HANDSHAKE; + static int quictypes; + + static final System.Logger logger; static { @@ -94,6 +132,12 @@ public abstract class Log implements System.Logger { case "headers": logging |= HEADERS; break; + case "quic": + logging |= QUIC; + break; + case "http3": + logging |= HTTP3; + break; case "content": logging |= CONTENT; break; @@ -107,13 +151,14 @@ public abstract class Log implements System.Logger { logging |= TRACE; break; case "all": - logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL| CHANNEL; + logging |= CONTENT | HEADERS | REQUESTS | FRAMES | ERRORS | TRACE | SSL | CHANNEL | QUIC | HTTP3; frametypes |= ALL; + quictypes |= QUIC_ALL; break; default: // ignore bad values } - if (val.startsWith("frames")) { + if (val.startsWith("frames:") || val.equals("frames")) { logging |= FRAMES; String[] types = val.split(":"); if (types.length == 1) { @@ -139,6 +184,56 @@ public abstract class Log implements System.Logger { } } } + if (val.startsWith("quic:") || val.equals("quic")) { + logging |= QUIC; + String[] types = val.split(":"); + if (types.length == 1) { + quictypes = QUIC_ALL & ~QUIC_TIMER & ~QUIC_DIRECT_BUFFER_POOL; + } else { + for (String type : types) { + switch (type.toLowerCase(Locale.US)) { + case "control": + quictypes |= QUIC_CONTROL; + break; + case "data": + quictypes |= QUIC_DATA; + break; + case "processed": + quictypes |= QUIC_PROCESSED; + break; + case "retransmit": + quictypes |= QUIC_RETRANSMIT; + break; + case "crypto": + quictypes |= QUIC_CRYPTO; + break; + case "cc": + quictypes |= QUIC_CC; + break; + case "hs": + quictypes |= QUIC_HANDSHAKE; + break; + case "ack": + quictypes |= QUIC_ACK; + break; + case "ping": + quictypes |= QUIC_PING; + break; + case "timer": + quictypes |= QUIC_TIMER; + break; + case "dbb": + quictypes |= QUIC_DIRECT_BUFFER_POOL; + break; + case "all": + quictypes = QUIC_ALL; + break; + default: + // ignore bad values + } + } + } + } } } if (logging != OFF) { @@ -175,6 +270,119 @@ public abstract class Log implements System.Logger { return (logging & CHANNEL) != 0; } + public static boolean altsvc() { return headers(); } + + public static boolean quicRetransmit() { + return (logging & QUIC) != 0 && (quictypes & QUIC_RETRANSMIT) != 0; + } + + // not called directly - but impacts isLogging(QuicFrame) + public static boolean quicHandshake() { + return (logging & QUIC) != 0 && (quictypes & QUIC_HANDSHAKE) != 0; + } + + public static boolean quicProcessed() { + return (logging & QUIC) != 0 && (quictypes & QUIC_PROCESSED) != 0; + } + + // not called directly - but impacts isLogging(QuicFrame) + public static boolean quicData() { + return (logging & QUIC) != 0 && (quictypes & QUIC_DATA) != 0; + } + + public static boolean quicCrypto() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CRYPTO) != 0; + } + + public static boolean quicCC() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CC) != 0; + } + + public static boolean quicControl() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CONTROL) != 0; + } + + public static boolean quicTimer() { + return (logging & QUIC) != 0 && (quictypes & QUIC_TIMER) != 0; + } + public static boolean quicDBB() { + return (logging & QUIC) != 0 && (quictypes & QUIC_DIRECT_BUFFER_POOL) != 0; + } + + public static boolean quic() { + return (logging & QUIC) != 0; + } + + public static boolean http3() { + return (logging & HTTP3) != 0; + } + + public static void logHttp3(String s, Object... s1) { + if (http3()) { + logger.log(Level.INFO, "HTTP3: " + s, s1); + } + } + + private static boolean isLogging(QuicFrame frame) { + if (frame instanceof StreamFrame sf) + return (quictypes & QUIC_DATA) != 0 + || (quictypes & QUIC_CONTROL) != 0 && sf.isLast() + || (quictypes & QUIC_CONTROL) != 0 && sf.offset() == 0; + if (frame instanceof AckFrame) + return (quictypes & QUIC_ACK) != 0; + if (frame instanceof CryptoFrame) + return (quictypes & QUIC_CRYPTO) != 0 + || (quictypes & QUIC_HANDSHAKE) != 0; + if (frame instanceof PingFrame) + return (quictypes & QUIC_PING) != 0; + if (frame instanceof PaddingFrame) return false; + if (frame instanceof HandshakeDoneFrame && quicHandshake()) + return true; + return (quictypes & QUIC_CONTROL) != 0; + } + + private static final EnumSet HS_TYPES = EnumSet.complementOf( + EnumSet.of(PacketType.ONERTT)); + + private static boolean quicPacketLoggable(QuicPacket packet) { + return (logging & QUIC) != 0 + && (quictypes == QUIC_ALL + || quicHandshake() && HS_TYPES.contains(packet.packetType()) + || stream(packet.frames()).anyMatch(Log::isLogging)); + } + + public static boolean quicPacketOutLoggable(QuicPacket packet) { + return quicPacketLoggable(packet); + } + + private static Stream stream(Collection list) { + return list == null ? Stream.empty() : list.stream(); + } + + public static boolean quicPacketInLoggable(QuicPacket packet) { + return quicPacketLoggable(packet); + } + + public static void logQuic(String s, Object... s1) { + if (quic()) { + logger.log(Level.INFO, "QUIC: " + s, s1); + } + } + + public static void logQuicPacketOut(String connectionTag, QuicPacket packet) { + if (quicPacketOutLoggable(packet)) { + logger.log(Level.INFO, "QUIC: {0} OUT: {1}", + connectionTag, packet.prettyPrint()); + } + } + + public static void logQuicPacketIn(String connectionTag, QuicPacket packet) { + if (quicPacketInLoggable(packet)) { + logger.log(Level.INFO, "QUIC: {0} IN: {1}", + connectionTag, packet.prettyPrint()); + } + } + public static void logError(String s, Object... s1) { if (errors()) { logger.log(Level.INFO, "ERROR: " + s, s1); @@ -237,6 +445,12 @@ public abstract class Log implements System.Logger { } } + public static void logAltSvc(String s, Object... s1) { + if (altsvc()) { + logger.log(Level.INFO, "ALTSVC: " + s, s1); + } + } + public static boolean loggingFrame(Class clazz) { if (frametypes == ALL) { return true; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java index 3aec13b59ec..ef031eef999 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java @@ -55,6 +55,8 @@ public final class OperationTrackers { long getOutstandingHttpOperations(); // The number of active HTTP/2 streams long getOutstandingHttp2Streams(); + // The number of active HTTP/3 streams + long getOutstandingHttp3Streams(); // The number of active WebSockets long getOutstandingWebSocketOperations(); // number of TCP connections still opened diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java index 489fbe7ffd8..c74c67f7d58 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java @@ -25,7 +25,6 @@ package jdk.internal.net.http.common; import java.time.Instant; -import java.time.InstantSource; /** * A {@link TimeLine} based on {@link System#nanoTime()} for the @@ -52,7 +51,7 @@ public final class TimeSource implements TimeLine { // The use of Integer.MAX_VALUE is arbitrary. // Any value not too close to Long.MAX_VALUE // would do. - static final int TIME_WINDOW = Integer.MAX_VALUE; + static final long TIME_WINDOW = Integer.MAX_VALUE; final Instant first; final long firstNanos; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index bcedff8844e..b14d76d8dba 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -37,33 +37,50 @@ import java.io.PrintStream; import java.io.UncheckedIOException; import java.lang.System.Logger.Level; import java.net.ConnectException; +import java.net.Inet6Address; import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardSocketOptions; import java.net.Proxy; import java.net.URI; import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; import java.net.http.HttpTimeoutException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.CancelledKeyException; +import java.nio.channels.NetworkChannel; import java.nio.channels.SelectionKey; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.text.Normalizer; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HexFormat; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CancellationException; +import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; import java.util.function.BiPredicate; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -152,6 +169,10 @@ public final class Utils { return prop.isEmpty() ? true : Boolean.parseBoolean(prop); } + // A threshold to decide whether to slice or copy. + // see sliceOrCopy + public static final int SLICE_THRESHOLD = 32; + /** * Allocated buffer size. Must never be higher than 16K. But can be lower * if smaller allocation units preferred. HTTP/2 mandates that all @@ -169,7 +190,8 @@ public final class Utils { private static Set getDisallowedHeaders() { Set headers = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade")); + headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade", + "alt-used")); String v = getNetProperty("jdk.httpclient.allowRestrictedHeaders"); if (v != null) { @@ -215,6 +237,56 @@ public final class Utils { return true; }; + public static T addSuppressed(T x, Throwable suppressed) { + if (x != suppressed && suppressed != null) { + var sup = x.getSuppressed(); + if (sup != null && sup.length > 0) { + if (Arrays.asList(sup).contains(suppressed)) { + return x; + } + } + sup = suppressed.getSuppressed(); + if (sup != null && sup.length > 0) { + if (Arrays.asList(sup).contains(x)) { + return x; + } + } + x.addSuppressed(suppressed); + } + return x; + } + + /** + * {@return a string comparing the given deadline with now, typically + * something like "due since Nms" or "due in Nms"} + * + * @apiNote + * This method recognize deadlines set to Instant.MIN + * and Instant.MAX as special cases meaning "due" and + * "not scheduled". + * + * @param now now + * @param deadline the deadline + */ + public static String debugDeadline(Deadline now, Deadline deadline) { + boolean isDue = deadline.compareTo(now) <= 0; + try { + if (isDue) { + if (deadline.equals(Deadline.MIN)) { + return "due (Deadline.MIN)"; + } else { + return "due since " + deadline.until(now, ChronoUnit.MILLIS) + "ms"; + } + } else if (deadline.equals(Deadline.MAX)) { + return "not scheduled (Deadline.MAX)"; + } else { + return "due in " + now.until(deadline, ChronoUnit.MILLIS) + "ms"; + } + } catch (ArithmeticException x) { + return isDue ? "due since too long" : "due in the far future"; + } + } + public record ProxyHeaders(HttpHeaders userHeaders, HttpHeaders systemHeaders) {} public static final BiPredicate PROXY_TUNNEL_RESTRICTED() { @@ -346,6 +418,7 @@ public final class Utils { } public static String interestOps(SelectionKey key) { + if (key == null) return "null-key"; try { return describeOps(key.interestOps()); } catch (CancelledKeyException x) { @@ -354,6 +427,7 @@ public final class Utils { } public static String readyOps(SelectionKey key) { + if (key == null) return "null-key"; try { return describeOps(key.readyOps()); } catch (CancelledKeyException x) { @@ -438,6 +512,21 @@ public final class Utils { return cause; } + public static IOException toIOException(Throwable cause) { + if (cause == null) return null; + if (cause instanceof CompletionException ce) { + cause = ce.getCause(); + } else if (cause instanceof ExecutionException ee) { + cause = ee.getCause(); + } + if (cause instanceof IOException io) { + return io; + } else if (cause instanceof UncheckedIOException uio) { + return uio.getCause(); + } + return new IOException(cause.getMessage(), cause); + } + public static IOException getIOException(Throwable t) { if (t instanceof IOException) { return (IOException) t; @@ -575,6 +664,10 @@ public final class Utils { return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); } + public static long getLongProperty(String name, long defaultValue) { + return Long.parseLong(System.getProperty(name, String.valueOf(defaultValue))); + } + public static int getIntegerNetProperty(String property, int min, int max, int defaultValue, boolean log) { int value = Utils.getIntegerNetProperty(property, defaultValue); // use default value if misconfigured @@ -610,6 +703,8 @@ public final class Utils { p1.setSNIMatchers(p.getSNIMatchers()); p1.setServerNames(p.getServerNames()); p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); + p1.setSignatureSchemes(p.getSignatureSchemes()); + p1.setNamedGroups(p.getNamedGroups()); return p1; } @@ -753,6 +848,91 @@ public final class Utils { return remain; } + // + + /** + * Reads as much bytes as possible from the buffer list, and + * write them in the provided {@code data} byte array. + * Returns the number of bytes read and written to the byte array. + * This method advances the position in the byte buffers it reads + * @param bufs A list of byte buffer + * @param data A byte array to write into + * @param offset Where to start writing in the byte array + * @return the amount of bytes read and written to the byte array + */ + public static int read(List bufs, byte[] data, int offset) { + int pos = offset; + for (ByteBuffer buf : bufs) { + if (pos >= data.length) break; + int read = Math.min(buf.remaining(), data.length - pos); + if (read <= 0) continue; + buf.get(data, pos, read); + pos += read; + } + return pos - offset; + } + + /** + * Returns the next buffer that has remaining bytes, or null. + * @param iterator an iterator + * @return the next buffer that has remaining bytes, or null + */ + public static ByteBuffer next(Iterator iterator) { + ByteBuffer next = null; + while (iterator.hasNext() && !(next = iterator.next()).hasRemaining()); + return next == null || !next.hasRemaining() ? null : next; + } + + /** + * Compute the relative consolidated position in bytes at which the two + * input mismatch, or -1 if there is no mismatch. + * @apiNote This method behaves as {@link ByteBuffer#mismatch(ByteBuffer)}. + * @param these a first list of byte buffers + * @param those a second list of byte buffers + * @return the relative consolidated position in bytes at which the two + * input mismatch, or -1L if there is no mismatch. + */ + public static long mismatch(List these, List those) { + if (these.isEmpty()) return those.isEmpty() ? -1 : 0; + if (those.isEmpty()) return 0; + Iterator lefti = these.iterator(), righti = those.iterator(); + ByteBuffer left = next(lefti), right = next(righti); + long parsed = 0; + while (left != null || right != null) { + int m = left == null || right == null ? 0 : left.mismatch(right); + if (m == -1) { + parsed = parsed + left.remaining(); + assert right.remaining() == left.remaining(); + if ((left = next(lefti)) != null) { + if ((right = next(righti)) != null) { + continue; + } + return parsed; + } + return (right = next(righti)) != null ? parsed : -1; + } + if (m == 0) return parsed; + parsed = parsed + m; + if (m < left.remaining()) { + if (m < right.remaining()) { + return parsed; + } + if ((right = next(righti)) != null) { + left = left.slice(m, left.remaining() - m); + continue; + } + return parsed; + } + assert m < right.remaining(); + if ((left = next(lefti)) != null) { + right = right.slice(m, right.remaining() - m); + continue; + } + return parsed; + } + return -1L; + } + public static long synchronizedRemaining(List bufs) { if (bufs == null) return 0L; synchronized (bufs) { @@ -764,12 +944,13 @@ public final class Utils { if (bufs == null) return 0; long remain = 0; for (ByteBuffer buf : bufs) { - remain += buf.remaining(); - if (remain > max) { + int size = buf.remaining(); + if (max - remain < size) { throw new IllegalArgumentException("too many bytes"); } + remain += size; } - return (int) remain; + return remain; } public static int remaining(List bufs, int max) { @@ -781,12 +962,13 @@ public final class Utils { if (refs == null) return 0; long remain = 0; for (ByteBuffer b : refs) { - remain += b.remaining(); - if (remain > max) { + int size = b.remaining(); + if (max - remain < size) { throw new IllegalArgumentException("too many bytes"); } + remain += size; } - return (int) remain; + return remain; } public static int remaining(ByteBuffer[] refs, int max) { @@ -832,6 +1014,50 @@ public final class Utils { return newb; } + /** + * Creates a slice of a buffer, possibly copying the data instead + * of slicing. + * If the buffer capacity is less than the {@linkplain #SLICE_THRESHOLD + * default slice threshold}, or if the capacity minus the length to slice + * is less than the {@linkplain #SLICE_THRESHOLD threshold}, returns a slice. + * Otherwise, copy so as not to retain a reference to a big buffer + * for a small slice. + * @param src the original buffer + * @param start where to start copying/slicing from src + * @param len how many byte to slice/copy + * @return a new ByteBuffer for the given slice + */ + public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len) { + return sliceOrCopy(src, start, len, SLICE_THRESHOLD); + } + + /** + * Creates a slice of a buffer, possibly copying the data instead + * of slicing. + * If the buffer capacity minus the length to slice is less than the threshold, + * returns a slice. + * Otherwise, copy so as not to retain a reference to a buffer + * that contains more bytes than needed. + * @param src the original buffer + * @param start where to start copying/slicing from src + * @param len how many byte to slice/copy + * @param threshold a threshold to decide whether to slice or copy + * @return a new ByteBuffer for the given slice + */ + public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len, int threshold) { + assert src.hasArray(); + int cap = src.array().length; + if (cap - len < threshold) { + return src.slice(start, len); + } else { + byte[] b = new byte[len]; + if (len > 0) { + src.get(start, b, 0, len); + } + return ByteBuffer.wrap(b); + } + } + /** * Get the Charset from the Content-encoding header. Defaults to * UTF_8 @@ -847,7 +1073,9 @@ public final class Utils { if (value == null) return StandardCharsets.UTF_8; return Charset.forName(value); } catch (Throwable x) { - Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + if (Log.trace()) { + Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + } return StandardCharsets.UTF_8; } } @@ -1076,6 +1304,40 @@ public final class Utils { } } + /** + * Creates HTTP/2 HTTP/3 pseudo headers for the given request. + * @param request the request + * @return pseudo headers for that request + */ + public static HttpHeaders createPseudoHeaders(HttpRequest request) { + HttpHeadersBuilder hdrs = new HttpHeadersBuilder(); + String method = request.method(); + hdrs.setHeader(":method", method); + URI uri = request.uri(); + hdrs.setHeader(":scheme", uri.getScheme()); + String host = uri.getHost(); + int port = uri.getPort(); + assert host != null; + if (port != -1) { + hdrs.setHeader(":authority", host + ":" + port); + } else { + hdrs.setHeader(":authority", host); + } + String query = uri.getRawQuery(); + String path = uri.getRawPath(); + if (path == null || path.isEmpty()) { + if (method.equalsIgnoreCase("OPTIONS")) { + path = "*"; + } else { + path = "/"; + } + } + if (query != null) { + path += "?" + query; + } + hdrs.setHeader(":path", Utils.encode(path)); + return hdrs.build(); + } // -- toAsciiString-like support to encode path and query URI segments // Encodes all characters >= \u0080 into escaped, normalized UTF-8 octets, @@ -1119,6 +1381,302 @@ public final class Utils { return sb.toString(); } + /** + * {@return the content of the buffer as an hexadecimal string} + * This method doesn't move the buffer position or limit. + * @param buffer a byte buffer + */ + public static String asHexString(ByteBuffer buffer) { + if (!buffer.hasRemaining()) return ""; + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(buffer.position(), bytes); + return HexFormat.of().formatHex(bytes); + } + + /** + * Converts a ByteBuffer containing bytes encoded using + * the given {@linkplain Charset charset} into a + * string. This method does not throw but will replace + * unrecognized sequences with the replacement character. + * The bytes in the buffer are consumed. + * + * @apiNote + * This method is intended for debugging purposes only, + * since buffers are not guaranteed to be split at character + * boundaries. + * + * @param buffer a buffer containing bytes encoded using + * a charset + * @param charset the charset to use to decode the bytes + * into a string + * + * @return a string built from the bytes contained + * in the buffer decoded using the given charset + */ + public static String asString(ByteBuffer buffer, Charset charset) { + var decoded = charset.decode(buffer); + char[] chars = new char[decoded.length()]; + decoded.get(chars); + return new String(chars); + } + + /** + * Converts a ByteBuffer containing UTF-8 bytes into a + * string. This method does not throw but will replace + * unrecognized sequences with the replacement character. + * The bytes in the buffer are consumed. + * + * @apiNote + * This method is intended for debugging purposes only, + * since buffers are not guaranteed to be split at character + * boundaries. + * + * @param buffer a buffer containing UTF-8 bytes + * + * @return a string built from the decoded UTF-8 bytes contained + * in the buffer + */ + public static String asString(ByteBuffer buffer) { + return asString(buffer, StandardCharsets.UTF_8); + } + + public static String millis(Instant now, Instant deadline) { + if (Instant.MAX.equals(deadline)) return "not scheduled"; + try { + long delay = now.until(deadline, ChronoUnit.MILLIS); + return delay + " ms"; + } catch (ArithmeticException a) { + return "too far away"; + } + } + + public static String millis(Deadline now, Deadline deadline) { + return millis(now.asInstant(), deadline.asInstant()); + } + + public static ExecutorService safeExecutor(ExecutorService delegate, + BiConsumer errorHandler) { + Executor overflow = new CompletableFuture().defaultExecutor(); + return new SafeExecutorService(delegate, overflow, errorHandler); + } + + public static sealed class SafeExecutor implements Executor + permits SafeExecutorService { + final E delegate; + final BiConsumer errorHandler; + final Executor overflow; + + public SafeExecutor(E delegate, Executor overflow, BiConsumer errorHandler) { + this.delegate = delegate; + this.overflow = overflow; + this.errorHandler = errorHandler; + } + + @Override + public void execute(Runnable command) { + ensureExecutedAsync(command); + } + + private void ensureExecutedAsync(Runnable command) { + try { + delegate.execute(command); + } catch (RejectedExecutionException t) { + errorHandler.accept(command, t); + overflow.execute(command); + } + } + + } + + public static final class SafeExecutorService extends SafeExecutor + implements ExecutorService { + + public SafeExecutorService(ExecutorService delegate, + Executor overflow, + BiConsumer errorHandler) { + super(delegate, overflow, errorHandler); + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + + @Override + public List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return delegate.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return delegate.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return delegate.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return delegate.invokeAny(tasks); + } + } + + public static T configureChannelBuffers(Consumer logSink, T chan, + int receiveBufSize, int sendBufSize) { + + if (logSink != null) { + int bufsize = getSoReceiveBufferSize(logSink, chan); + logSink.accept("Initial receive buffer size is: %d".formatted(bufsize)); + bufsize = getSoSendBufferSize(logSink, chan); + logSink.accept("Initial send buffer size is: %d".formatted(bufsize)); + } + if (trySetReceiveBufferSize(logSink, chan, receiveBufSize)) { + if (logSink != null) { + int bufsize = getSoReceiveBufferSize(logSink, chan); + logSink.accept("Receive buffer size configured: %d".formatted(bufsize)); + } + } + if (trySetSendBufferSize(logSink, chan, sendBufSize)) { + if (logSink != null) { + int bufsize = getSoSendBufferSize(logSink, chan); + logSink.accept("Send buffer size configured: %d".formatted(bufsize)); + } + } + return chan; + } + + public static boolean trySetReceiveBufferSize(Consumer logSink, NetworkChannel chan, int bufsize) { + try { + if (bufsize > 0) { + chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize); + return true; + } + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to set receive buffer size to %d on %s" + .formatted(bufsize, chan)); + } + return false; + } + + public static boolean trySetSendBufferSize(Consumer logSink, NetworkChannel chan, int bufsize) { + try { + if (bufsize > 0) { + chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize); + return true; + } + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to set send buffer size to %d on %s" + .formatted(bufsize, chan)); + } + return false; + } + + public static int getSoReceiveBufferSize(Consumer logSink, NetworkChannel chan) { + try { + return chan.getOption(StandardSocketOptions.SO_RCVBUF); + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan)); + } + return 0; + } + + public static int getSoSendBufferSize(Consumer logSink, NetworkChannel chan) { + try { + return chan.getOption(StandardSocketOptions.SO_SNDBUF); + } catch (IOException x) { + if (logSink!= null) + logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan)); + } + return 0; + } + + + /** + * Try to figure out whether local and remote addresses are compatible. + * Used to diagnose potential communication issues early. + * This is a best effort, and there is no guarantee that all potential + * conflicts will be detected. + * @param local local address + * @param peer peer address + * @return a message describing the conflict, if any, or {@code null} if no + * conflict was detected. + */ + public static String addressConflict(SocketAddress local, SocketAddress peer) { + if (local == null || peer == null) return null; + if (local.equals(peer)) { + return "local endpoint and remote endpoint are bound to the same IP address and port"; + } + if (!(local instanceof InetSocketAddress li) || !(peer instanceof InetSocketAddress pi)) { + return null; + } + var laddr = li.getAddress(); + var paddr = pi.getAddress(); + if (!laddr.isAnyLocalAddress() && !paddr.isAnyLocalAddress()) { + if (laddr.getClass() != paddr.getClass()) { // IPv4 vs IPv6 + if ((laddr instanceof Inet6Address laddr6 && !laddr6.isIPv4CompatibleAddress()) + || (paddr instanceof Inet6Address paddr6 && !paddr6.isIPv4CompatibleAddress())) { + return "local endpoint IP (%s) and remote endpoint IP (%s) don't match" + .formatted(laddr.getClass().getSimpleName(), + paddr.getClass().getSimpleName()); + } + } + } + if (li.getPort() != pi.getPort()) return null; + if (li.getAddress().isAnyLocalAddress() && pi.getAddress().isLoopbackAddress()) { + return "local endpoint (wildcard) and remote endpoint (loopback) ports conflict"; + } + if (pi.getAddress().isAnyLocalAddress() && li.getAddress().isLoopbackAddress()) { + return "local endpoint (loopback) and remote endpoint (wildcard) ports conflict"; + } + return null; + } + /** * {@return the exception the given {@code cf} was completed with, * or a {@link CancellationException} if the given {@code cf} was diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java new file mode 100644 index 00000000000..46d4ebeb772 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Optional; + +public final class AltSvcFrame extends Http2Frame { + + public static final int TYPE = 0xa; + + private final int length; + private final String origin; + private final String altSvcValue; + + private static final Charset encoding = StandardCharsets.US_ASCII; + + // Strings should be US-ASCII. This is checked by the FrameDecoder. + public AltSvcFrame(int streamid, int flags, Optional originVal, String altValue) { + super(streamid, flags); + this.origin = originVal.orElse(""); + this.altSvcValue = Objects.requireNonNull(altValue); + this.length = 2 + origin.length() + altValue.length(); + assert origin.length() == origin.getBytes(encoding).length; + assert altSvcValue.length() == altSvcValue.getBytes(encoding).length; + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return length; + } + + public String getOrigin() { + return origin; + } + + public String getAltSvcValue() { + return altSvcValue; + } + + @Override + public String toString() { + return super.toString() + + ", origin=" + this.origin + + ", alt-svc: " + altSvcValue; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java index 7ebfa090830..da05f6392c1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,11 +26,13 @@ package jdk.internal.net.http.frame; import java.io.IOException; -import java.lang.System.Logger.Level; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.Utils; @@ -344,6 +346,8 @@ public class FramesDecoder { return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags); case ContinuationFrame.TYPE: return parseContinuationFrame(frameLength, frameStreamid, frameFlags); + case AltSvcFrame.TYPE: + return parseAltSvcFrame(frameLength, frameStreamid, frameFlags); default: // RFC 7540 4.1 // Implementations MUST ignore and discard any frame that has a type that is unknown. @@ -557,4 +561,32 @@ public class FramesDecoder { return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength)); } + private Http2Frame parseAltSvcFrame(int frameLength, int frameStreamid, int frameFlags) { + var len = getShort(); + byte[] origin; + Optional originUri = Optional.empty(); + if (len > 0) { + origin = getBytes(len); + if (!isUSAscii(origin)) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid, + "illegal character in AltSvcFrame"); + } + originUri = Optional.of(new String(origin, StandardCharsets.US_ASCII)); + } + byte[] altbytes = getBytes(frameLength - 2 - len); + if (!isUSAscii(altbytes)) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid, + "illegal character in AltSvcFrame"); + } + String altSvc = new String(altbytes, StandardCharsets.US_ASCII); + return new AltSvcFrame(frameStreamid, 0, originUri, altSvc); + } + + static boolean isUSAscii(byte[] bytes) { + for (int i=0; i < bytes.length; i++) { + if (bytes[i] < 0) return false; + } + return true; + } + } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java index 4fdd4acd661..2ee2083c22c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java @@ -26,6 +26,7 @@ package jdk.internal.net.http.frame; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -70,6 +71,7 @@ public class FramesEncoder { case GoAwayFrame.TYPE -> encodeGoAwayFrame((GoAwayFrame) frame); case WindowUpdateFrame.TYPE -> encodeWindowUpdateFrame((WindowUpdateFrame) frame); case ContinuationFrame.TYPE -> encodeContinuationFrame((ContinuationFrame) frame); + case AltSvcFrame.TYPE -> encodeAltSvcFrame((AltSvcFrame) frame); default -> throw new UnsupportedOperationException("Not supported frame " + frame.type() + " (" + frame.getClass().getName() + ")"); }; @@ -227,6 +229,20 @@ public class FramesEncoder { return join(buf, frame.getHeaderBlock()); } + private List encodeAltSvcFrame(AltSvcFrame frame) { + final int length = frame.length(); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, AltSvcFrame.TYPE, NO_FLAGS, frame.streamid); + final String origin = frame.getOrigin(); + assert (origin.length() & 0xffff0000) == 0; + buf.putShort((short)origin.length()); + if (!origin.isEmpty()) + buf.put(frame.getOrigin().getBytes(StandardCharsets.US_ASCII)); + buf.put(frame.getAltSvcValue().getBytes(StandardCharsets.US_ASCII)); + buf.flip(); + return List.of(buf); + } + private List joinWithPadding(ByteBuffer buf, List data, int padLength) { int len = data.size(); if (len == 0) return List.of(buf, getPadding(padLength)); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java index f837645696f..469d06cef0c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java @@ -91,8 +91,9 @@ public abstract class Http2Frame { case PingFrame.TYPE -> "PING"; case PushPromiseFrame.TYPE -> "PUSH_PROMISE"; case WindowUpdateFrame.TYPE -> "WINDOW_UPDATE"; + case AltSvcFrame.TYPE -> "ALTSVC"; - default -> "UNKNOWN"; + default -> "UNKNOWN"; }; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java index 881be12c67c..9cdd604efd6 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java @@ -282,7 +282,7 @@ public final class Decoder { if (endOfHeaderBlock && state != State.READY) { logger.log(NORMAL, () -> format("unexpected end of %s representation", state)); - throw new IOException("Unexpected end of header block"); + throw new ProtocolException("Unexpected end of header block"); } if (endOfHeaderBlock) { size = indexed = 0; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java index a233e0f3a38..979c3ded2bc 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,9 +40,10 @@ import java.nio.ByteBuffer; // // The encoding is simple and well known: 1 byte <-> 1 char // -final class ISO_8859_1 { +public final class ISO_8859_1 { - private ISO_8859_1() { } + private ISO_8859_1() { + } public static final class Reader { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java index 427c2504de5..c6b4c51761b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java @@ -619,7 +619,7 @@ public final class QuickHuffman { } } - static final class Reader implements Huffman.Reader { + public static final class Reader implements Huffman.Reader { private final BufferUpdateConsumer UPDATER = (buf, bufLen) -> { @@ -703,7 +703,7 @@ public final class QuickHuffman { } } - static final class Writer implements Huffman.Writer { + public static final class Writer implements Huffman.Writer { private final BufferUpdateConsumer UPDATER = (buf, bufLen) -> { @@ -782,12 +782,26 @@ public final class QuickHuffman { @Override public int lengthOf(CharSequence value, int start, int end) { - int len = 0; - for (int i = start; i < end; i++) { - char c = value.charAt(i); - len += codeLengthOf(c); - } - return bytesForBits(len); + return QuickHuffman.lengthOf(value, start, end); } } + + public static int lengthOf(CharSequence value, int start, int end) { + int len = 0; + for (int i = start; i < end; i++) { + char c = value.charAt(i); + len += codeLengthOf(c); + } + return bytesForBits(len); + } + + public static int lengthOf(CharSequence value) { + return lengthOf(value, 0, value.length()); + } + + /* Used to calculate the number of bytes required for Huffman encoding */ + + public static boolean isHuffmanBetterFor(CharSequence input) { + return lengthOf(input) < input.length(); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java new file mode 100644 index 00000000000..ca734d74ae7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3; + +import java.util.Objects; + +import jdk.internal.net.http.http3.frames.SettingsFrame; + +/** + * Represents the settings that are conveyed in a HTTP3 SETTINGS frame for a HTTP3 connection + */ +public record ConnectionSettings( + long maxFieldSectionSize, + long qpackMaxTableCapacity, + long qpackBlockedStreams) { + + // we use -1 (an internal value) to represent unlimited + public static final long UNLIMITED_MAX_FIELD_SECTION_SIZE = -1; + + public static ConnectionSettings createFrom(final SettingsFrame frame) { + Objects.requireNonNull(frame); + // default is unlimited as per RFC-9114 section 7.2.4.1 + final long maxFieldSectionSize = getOrDefault(frame, SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, + UNLIMITED_MAX_FIELD_SECTION_SIZE); + // default is zero as per RFC-9204 section 5 + final long qpackMaxTableCapacity = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0); + // default is zero as per RFC-9204, section 5 + final long qpackBlockedStreams = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS, 0); + return new ConnectionSettings(maxFieldSectionSize, qpackMaxTableCapacity, qpackBlockedStreams); + } + + private static long getOrDefault(final SettingsFrame frame, final int paramId, final long defaultValue) { + final long val = frame.getParameter(paramId); + if (val == -1) { + return defaultValue; + } + return val; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java new file mode 100644 index 00000000000..423fb27f844 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3; + +import java.util.HexFormat; +import java.util.Optional; +import java.util.stream.Stream; + +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * This enum models HTTP/3 error codes as specified in + * RFC 9114, Section 8, + * augmented with QPack error codes as specified in + * RFC 9204, Section 6. + */ +public enum Http3Error { + + /** + * No error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * This is used when the connection or stream
    +     * needs to be closed, but there is no error to signal.
    +     * }
    + */ + H3_NO_ERROR (0x0100), // 256 + + /** + * General protocol error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * Peer violated protocol requirements in a way that does
    +     * not match a more specific error code, or endpoint declines
    +     * to use the more specific error code.
    +     * }
    + */ + H3_GENERAL_PROTOCOL_ERROR (0x0101), // 257 + + /** + * Internal error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * An internal error has occurred in the HTTP stack.
    +     * }
    + */ + H3_INTERNAL_ERROR (0x0102), // 258 + + /** + * Stream creation error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * The endpoint detected that its peer created a stream that
    +     * it will not accept.
    +     * }
    + */ + H3_STREAM_CREATION_ERROR (0x0103), // 259 + + /** + * Critical stream closed error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * A stream required by the HTTP/3 connection was closed or reset.
    +     * }
    + */ + H3_CLOSED_CRITICAL_STREAM (0x0104), // 260 + + /** + * Frame unexpected error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * A frame was received that was not permitted in the
    +     * current state or on the current stream.
    +     * }
    + */ + H3_FRAME_UNEXPECTED (0x0105), // 261 + + /** + * Frame error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * A frame that fails to satisfy layout requirements or with
    +     * an invalid size was received.
    +     * }
    + */ + H3_FRAME_ERROR (0x0106), // 262 + + /** + * Excessive load error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * The endpoint detected that its peer is exhibiting a behavior
    +     * that might be generating excessive load.
    +     * }
    + */ + H3_EXCESSIVE_LOAD (0x0107), // 263 + + /** + * Stream ID or Push ID error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * A Stream ID or Push ID was used incorrectly, such as exceeding
    +     * a limit, reducing a limit, or being reused.
    +     * }
    + */ + H3_ID_ERROR (0x0108), // 264 + + /** + * Settings error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * An endpoint detected an error in the payload of a SETTINGS frame.
    +     * }
    + */ + H3_SETTINGS_ERROR (0x0109), // 265 + + /** + * Missing settings error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * No SETTINGS frame was received at the beginning of the control
    +     * stream.
    +     * }
    + */ + H3_MISSING_SETTINGS (0x010a), // 266 + + /** + * Request rejected error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * A server rejected a request without performing any application
    +     * processing.
    +     * }
    + */ + H3_REQUEST_REJECTED (0x010b), // 267 + + /** + * Request cancelled error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * The request or its response (including pushed response) is
    +     * cancelled.
    +     * }
    + */ + H3_REQUEST_CANCELLED (0x010c), // 268 + + /** + * Request incomplete error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * The client's stream terminated without containing a
    +     * fully-formed request.
    +     * }
    + */ + H3_REQUEST_INCOMPLETE (0x010d), //269 + + /** + * Message error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * An HTTP message was malformed and cannot be processed.
    +     * }
    + */ + H3_MESSAGE_ERROR (0x010e), // 270 + + /** + * Connect error. + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * The TCP connection established in response to a CONNECT
    +     * request was reset or abnormally closed.
    +     * }
    + */ + H3_CONNECT_ERROR (0x010f), // 271 + + /** + * Version fallback error + *

    + * From + * RFC 9114, Section 8.1: + *

    {@code
    +     * The requested operation cannot be served over HTTP/3.
    +     * The peer should retry over HTTP/1.1.
    +     * }
    + */ + H3_VERSION_FALLBACK (0x0110), // 272 + + /** + * QPack decompression error + *

    + * From + * RFC 9204, Section 6: + *

    {@code
    +     * The decoder failed to interpret an encoded field section
    +     * and is not able to continue decoding that field section.
    +     * }
    + */ + QPACK_DECOMPRESSION_FAILED (0x0200), // 512 + + /** + * Qpack encoder stream error. + *

    + * From + * RFC 9204, Section 6: + *

    {@code
    +     * The decoder failed to interpret an encoder instruction
    +     * received on the encoder stream.
    +     * }
    + */ + QPACK_ENCODER_STREAM_ERROR (0x0201), // 513 + + /** + * Qpack decoder stream error + *

    + * From + * RFC 9204, Section 6: + *

    {@code
    +     * The encoder failed to interpret a decoder instruction
    +     * received on the decoder stream.
    +     * }
    + */ + QPACK_DECODER_STREAM_ERROR (0x0202); // 514 + + final long errorCode; + Http3Error(long errorCode) { + this.errorCode = errorCode; + } + + public long code() { + return errorCode; + } + + public static Optional fromCode(long code) { + return Stream.of(values()).filter((v) -> v.code() == code) + .findFirst(); + } + + public static String stringForCode(long code) { + return fromCode(code).map(Http3Error::name).orElse(unknown(code)); + } + + private static String unknown(long code) { + return "UnknownError(code=0x" + HexFormat.of().withUpperCase().toHexDigits(code) + ")"; + } + + /** + * {@return true if the given code is {@link Http3Error#H3_NO_ERROR} or equivalent} + * Unknown error codes are treated as equivalent to {@code H3_NO_ERROR} + * @param code an HTTP/3 code error code + */ + public static boolean isNoError(long code) { + return fromCode(code).orElse(H3_NO_ERROR) == Http3Error.H3_NO_ERROR; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java new file mode 100644 index 00000000000..4cb8aa051fd --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Random; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.http3.frames.Http3FrameType.asString; + +/** + * Super class for all HTTP/3 frames. + */ +public abstract non-sealed class AbstractHttp3Frame implements Http3Frame { + public static final Random RANDOM = new Random(); + final long type; + public AbstractHttp3Frame(long type) { + this.type = type; + } + + public final String typeAsString() { + return asString(type()); + } + + @Override + public long type() { + return type; + } + + + /** + * Computes the size of this frame. This corresponds to + * the {@linkplain #length()} of the frame's payload, plus the + * size needed to encode this length, plus the size needed to + * encode the frame type. + * + * @return the size of this frame. + */ + public long size() { + var len = length(); + return len + VariableLengthEncoder.getEncodedSize(len) + + VariableLengthEncoder.getEncodedSize(type()); + } + + public int headersSize() { + var len = length(); + return VariableLengthEncoder.getEncodedSize(len) + + VariableLengthEncoder.getEncodedSize(type()); + } + + @Override + public long streamingLength() { + return 0; + } + + protected static long decodeRequiredType(final BuffersReader reader, final long expectedType) { + final long type = VariableLengthEncoder.decode(reader); + if (type < 0) throw new BufferUnderflowException(); + // TODO: throw an exception instead? + assert type == expectedType : "bad frame type: " + type + " expected: " + expectedType; + return type; + } + + protected static MalformedFrame checkPayloadSize(long frameType, + BuffersReader reader, + long start, + long length) { + // check position after reading payload + long read = reader.position() - start; + if (length != read) { + reader.position(start + length); + reader.release(); + return new MalformedFrame(frameType, + Http3Error.H3_FRAME_ERROR.code(), + "payload length mismatch (length=%s, read=%s)" + .formatted(length, start)); + + } + + assert length == reader.position() - start; + return null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(typeAsString()) + .append(": length=") + .append(length()); + return sb.toString(); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java new file mode 100644 index 00000000000..f470efb9b27 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents the CANCEL_PUSH HTTP3 frame + */ +public final class CancelPushFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.CANCEL_PUSH_FRAME; + private final long length; + private final long pushId; + + public CancelPushFrame(final long pushId) { + super(Http3FrameType.CANCEL_PUSH.type()); + this.pushId = pushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.pushId); + } + + // only used when constructing the frame during decoding content over a stream + private CancelPushFrame(final long pushId, final long length) { + super(Http3FrameType.CANCEL_PUSH.type()); + this.pushId = pushId; + this.length = length; + } + + @Override + public long length() { + return this.length; + } + + public long getPushId() { + return pushId; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the push id that needs to be cancelled + VariableLengthEncoder.encode(buf, this.pushId); + } + + /** + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * @param reader the reader + * @param debug a logger for debugging purposes + * @return the new frame + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + long position = reader.position(); + decodeRequiredType(reader, TYPE); + long length = VariableLengthEncoder.decode(reader); + if (length > reader.remaining() || length < 0) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed pushId + return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in CANCEL_PUSH frame: " + length); + } + + long pushId = VariableLengthEncoder.decode(reader); + if (pushId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new CancelPushFrame(pushId); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java new file mode 100644 index 00000000000..97b475774b2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + + +/** + * This class models an HTTP/3 DATA frame. + * @apiNote + * An instance of {@code DataFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see PartialFrame + */ +public final class DataFrame extends PartialFrame { + + /** + * The DATA frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.DATA_FRAME; + + private final long length; + + /** + * Creates a new HTTP/3 HEADERS frame + */ + public DataFrame(long length) { + super(TYPE, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java new file mode 100644 index 00000000000..a51c71e0a05 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.LongPredicate; +import java.util.function.Supplier; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; + +/** + * A FramesDecoder accumulates buffers until a frame can be + * decoded. It also supports decoding {@linkplain PartialFrame + * partial frames} and {@linkplain #readPayloadBytes() reading + * their payload} incrementally. + * @apiNote + * When the frame decoder {@linkplain #poll() returns} a partial + * frame, the same frame will be returned until its payload has been + * {@linkplain PartialFrame#remaining() fully} {@linkplain #readPayloadBytes() + * read}. + * The caller is supposed to call {@link #readPayloadBytes()} until + * {@link #poll()} returns a different frame. At this point there will be no + * {@linkplain PartialFrame#remaining() remaining} payload bytes to read for + * the previous frame. + *
    + * The sequence of calls: {@snippet : + * framesDecoder.submit(buffer); + * while ((frame = framesDecoder.poll()) != null) { + * if (frame instanceof PartialFrame partial) { + * var nextPayloadBytes = framesDecoder.readPayloadBytes(); + * if (nextPayloadBytes == null || nextPayloadBytes.isEmpty()) { + * // no more data is available at this moment + * break; + * } + * // nextPayloadBytes are the next bytes for the payload + * // of the partial frame + * deliverBytes(partial, nextPayloadBytes); + * } else ... + * // got a full frame... + * } + * } + * makes it possible to incrementally deliver payload bytes for + * a frame - since {@code poll()} will always return the same partial + * frame until all its payload has been read. + */ +public class FramesDecoder { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final ListBuffersReader framesReader = BuffersReader.list(); + private final ReentrantLock lock = new ReentrantLock(); + + private final Supplier dbgTag; + private final LongPredicate isAllowed; + + // the current partial frame or null + PartialFrame partialFrame; + boolean eof; + + /** + * A new {@code FramesDecoder} that accepts all frames. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(String dbgTag) { + this(dbgTag, FramesDecoder::allAllowed); + } + + /** + * A new {@code FramesDecoder} that accepts only frames + * authorized by the given {@code isAllowed} predicate. + * If a frame is not allowed, a {@link MalformedFrame} is + * returned. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(String dbgTag, LongPredicate isAllowed) { + this(() -> dbgTag, Objects.requireNonNull(isAllowed)); + } + + /** + * A new {@code FramesDecoder} that accepts only frames + * authorized by the given {@code isAllowed} predicate. + * If a frame is not allowed, a {@link MalformedFrame} is + * returned. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(Supplier dbgTag, LongPredicate isAllowed) { + this.dbgTag = dbgTag; + this.isAllowed = Objects.requireNonNull(isAllowed); + } + + String dbgTag() { return dbgTag.get(); } + + /** + * Submit a new buffer to this frames decoder + * @param buffer a new buffer from the stream + */ + public void submit(ByteBuffer buffer) { + lock.lock(); + try { + if (buffer == QuicStreamReader.EOF) { + eof = true; + } else { + framesReader.add(buffer); + } + } finally { + lock.unlock(); + } + } + + /** + * {@return an {@code Http3Frame}, possibly {@linkplain PartialFrame partial}, + * or {@code null} if not enough bytes have been receive to decode (at least + * partially) a frame} + * If a frame is illegal or not allowed, a {@link MalformedFrame} is + * returned. The caller is supposed to {@linkplain #clear() clear} all data + * and proceed to close the connection in that case. + */ + public Http3Frame poll() { + lock.lock(); + try { + if (partialFrame != null) { + if (partialFrame.remaining() != 0) { + return partialFrame; + } else partialFrame = null; + } + var frame = Http3Frame.decode(framesReader, this::isAllowed, debug); + if (frame instanceof PartialFrame partial) { + partialFrame = partial; + } + return frame; + } finally { + lock.unlock(); + } + } + + /** + * {@return the next payload bytes for the current partial frame, + * or {@code null} if no partial frame} + * If EOF has been reached ({@link QuicStreamReader#EOF EOF} was + * {@linkplain #submit(ByteBuffer) submitted}, and all buffers have + * been read, the returned list will contain {@link QuicStreamReader#EOF + * EOF} + */ + public List readPayloadBytes() { + lock.lock(); + try { + if (partialFrame == null || partialFrame.remaining() == 0) { + partialFrame = null; + return null; + } + if (eof && !framesReader.hasRemaining()) { + return List.of(QuicStreamReader.EOF); + } + return partialFrame.nextPayloadBytes(framesReader); + } finally { + lock.unlock(); + } + } + + /** + * {@return true if EOF has been reached and all buffers have been read} + */ + public boolean eof() { + lock.lock(); + try { + if (!eof) return false; + if (!framesReader.hasRemaining()) return true; + if (partialFrame != null) { + // still some payload data to read... + if (partialFrame.remaining() > 0) return false; + } + var pos = framesReader.position(); + try { + // if there's not enough data to decode a new frame or a new + // partial frame then since no more data will ever come, we do have + // reached EOF. If however, we can read a frame from the remaining + // data in the buffer, then EOF is not reached yet. + // The next call to poll() will return that frame. + var frame = Http3Frame.decode(framesReader, this::isAllowed, debug); + return frame == null; + } finally { + // restore position for the next call to poll. + framesReader.position(pos); + } + } finally { + lock.unlock(); + } + } + + /** + * {@return true if all buffers have been read} + */ + public boolean clean() { + lock.lock(); + try { + if (partialFrame != null) { + // still some payload data to read... + if (partialFrame.remaining() > 0) return false; + } + return !framesReader.hasRemaining(); + } finally { + lock.unlock(); + } + } + + /** + * Clears any unconsumed buffers. + */ + public void clear() { + lock.lock(); + try { + partialFrame = null; + framesReader.clear(); + } finally { + lock.unlock(); + } + } + + /** + * Can be overridden by subclasses to avoid parsing a frame + * fully if the frame is not allowed on this stream, or + * according to the stream state. + * + * @implSpec + * This method delegates to the {@linkplain #FramesDecoder(String, LongPredicate) + * predicate} given at construction time. If {@linkplain #FramesDecoder(String) + * no predicate} was given this method returns true. + * + * @param frameType the frame type + * @return true if the frame is allowed + */ + protected boolean isAllowed(long frameType) { + return isAllowed.test(frameType); + } + + /** + * A predicate that returns true for all frames types allowed + * on the server->client control stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a control stream. + */ + public static boolean isAllowedOnControlStream(long frameType) { + if (frameType == Http3FrameType.DATA.type()) return false; + if (frameType == Http3FrameType.HEADERS.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + /** + * A predicate that returns true for all frames types allowed + * on the client->server control stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a control stream. + */ + public static boolean isAllowedOnClientControlStream(long frameType) { + if (frameType == Http3FrameType.DATA.type()) return false; + if (frameType == Http3FrameType.HEADERS.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + /** + * A predicate that returns true for all frames types allowed + * on a request/response stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a request/response + * stream. + */ + public static boolean isAllowedOnRequestStream(long frameType) { + if (frameType == Http3FrameType.SETTINGS.type()) return false; + if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false; + if (frameType == Http3FrameType.GOAWAY.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + + /** + * A predicate that returns true for all frames types allowed + * on a push promise stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a request/response + * stream. + */ + public static boolean isAllowedOnPromiseStream(long frameType) { + if (frameType == Http3FrameType.SETTINGS.type()) return false; + if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false; + if (frameType == Http3FrameType.GOAWAY.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + private static boolean allAllowed(long frameType) { + return true; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java new file mode 100644 index 00000000000..274a1af3a56 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a GOAWAY HTTP3 frame + */ +public final class GoAwayFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.GOAWAY_FRAME; + private final long length; + // represents either a stream id or a push id depending on the context + // of the frame + private final long id; + + public GoAwayFrame(final long id) { + super(TYPE); + this.id = id; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.id); + } + + // only used when constructing the frame during decoding content over a stream + private GoAwayFrame(final long length, final long id) { + super(Http3FrameType.GOAWAY.type()); + this.length = length; + this.id = id; + } + + @Override + public long length() { + return this.length; + } + + /** + * {@return the id of either the stream or a push promise, depending on the context + * of this frame} + */ + public long getTargetId() { + return this.id; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the stream id/push id + VariableLengthEncoder.encode(buf, this.id); + } + + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + final long position = reader.position(); + // read the frame type + decodeRequiredType(reader, Http3FrameType.GOAWAY.type()); + // read length of the payload + final long length = VariableLengthEncoder.decode(reader); + if (length < 0 || length > reader.remaining()) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed targetId + return new MalformedFrame(TYPE, + Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in GOAWAY frame: " + length); + } + + // read stream id / push id + final long targetId = VariableLengthEncoder.decode(reader); + if (targetId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new GoAwayFrame(length, targetId); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(super.toString()).append(" stream/push id: ").append(this.id); + return sb.toString(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java new file mode 100644 index 00000000000..5d7672458a3 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +/** + * This class models an HTTP/3 HEADERS frame. + * @apiNote + * An instance of {@code HeadersFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see jdk.internal.net.http.http3.frames.PartialFrame + */ +public final class HeadersFrame extends PartialFrame { + + /** + * The HEADERS frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.HEADERS_FRAME; + + + private final long length; + + /** + * Creates a new HTTP/3 HEADERS frame + */ + public HeadersFrame(long length) { + super(TYPE, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java new file mode 100644 index 00000000000..c4b234553ad --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.util.function.LongPredicate; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import static jdk.internal.net.http.http3.frames.Http3FrameType.DATA; +import static jdk.internal.net.http.http3.frames.Http3FrameType.HEADERS; +import static jdk.internal.net.http.http3.frames.Http3FrameType.PUSH_PROMISE; +import static jdk.internal.net.http.http3.frames.Http3FrameType.UNKNOWN; +import static jdk.internal.net.http.http3.frames.Http3FrameType.asString; +import static jdk.internal.net.http.http3.frames.Http3FrameType.isIllegalType; + +/** + * An HTTP/3 frame + */ +public sealed interface Http3Frame permits AbstractHttp3Frame { + + /** + * {@return the type of this frame} + */ + long type(); + + /** + * {@return the length of this frame} + */ + long length(); + + + /** + * {@return the portion of the frame payload that can be read + * after the frame was created, when the current frame + * can be read as a partial frame, otherwise 0, if + * the payload can't be streamed} + */ + default long streamingLength() { return 0;} + + /** + * Attempts to decode an HTTP/3 frame from the bytes accumulated + * in the reader. + * + * @apiNote + * + * If an error is detected while parsing the frame, a {@link MalformedFrame} + * error will be returned + * + * @param reader the reader containing the bytes + * @param isFrameTypeAllowed a predicate to test whether a given + * frame type is allowed in this context + * @param debug a logger to log debug traces + * @return the decoded frame, or {@code null} if some bytes are + * missing to decode the frame + */ + static Http3Frame decode(BuffersReader reader, LongPredicate isFrameTypeAllowed, Logger debug) { + long pos = reader.position(); + long limit = reader.limit(); + long remaining = reader.remaining(); + long type = -1; + long before = reader.read(); + Http3Frame frame; + try { + int tsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos); + if (tsize == -1 || remaining - tsize < 0) return null; + type = VariableLengthEncoder.peekEncodedValue(reader, pos); + if (type == -1) return null; + if (isIllegalType(type) || !isFrameTypeAllowed.test(type)) { + var msg = "H3_FRAME_UNEXPECTED: Frame " + + asString(type) + + " is not allowed on this stream"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_UNEXPECTED.code(), msg); + reader.clear(); + return frame; + } + + int lsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize); + if (lsize == -1 || remaining - tsize - lsize < 0) return null; + final long length = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize); + var frameType = Http3FrameType.forType(type); + if (debug.on()) { + debug.log("Decoding %s(length=%s)", frameType, length); + } + if (frameType == UNKNOWN) { + if (debug.on()) { + debug.log("decode partial unknown frame: " + + "pos:%s, limit:%s, remaining:%s," + + " tsize:%s, lsize:%s, length:%s", + pos, limit, remaining, tsize, lsize, length); + } + reader.position(pos + tsize + lsize); + reader.release(); + return new UnknownFrame(type, length); + } else if (frameType.maxLength() < length) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too long"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + + if (frameType == HEADERS) { + if (length == 0) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + reader.position(pos + tsize + lsize); + reader.release(); + return new HeadersFrame(length); + } + + if (frameType == DATA) { + reader.position(pos + tsize + lsize); + reader.release(); + return new DataFrame(length); + } + + if (frameType == PUSH_PROMISE) { + int pidsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize + lsize); + if (length == 0 || length < pidsize) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too short to fit pushID"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + if (length == pidsize) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + if (pidsize == -1 || remaining - tsize - lsize - pidsize < 0) return null; + long pushId = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize + lsize); + reader.position(pos + tsize + lsize + pidsize); + reader.release(); + return new PushPromiseFrame(pushId, length - pidsize); + } + + if (length + tsize + lsize > reader.remaining()) { + // we haven't moved the reader's position. + // we'll be called back when new bytes are available and + // we'll resume reading type + length from the same position + // again, until we have enough to read the frame. + return null; + } + + assert isFrameTypeAllowed.test(type); + + frame = switch(frameType) { + case SETTINGS -> SettingsFrame.decodeFrame(reader, debug); + case GOAWAY -> GoAwayFrame.decodeFrame(reader, debug); + case CANCEL_PUSH -> CancelPushFrame.decodeFrame(reader, debug); + case MAX_PUSH_ID -> MaxPushIdFrame.decodeFrame(reader, debug); + default -> { + reader.position(pos + tsize + lsize); + reader.release(); + yield new UnknownFrame(type, length); + } + }; + + long read; + if (frame instanceof MalformedFrame || frame == null) { + return frame; + } else if ((read = (reader.read() - before - tsize - lsize)) != length) { + String msg = ("H3_FRAME_ERROR: Frame %s payload length does not match" + + " frame length (length=%s, payload=%s)") + .formatted(asString(type), length, read); + if (debug.on()) debug.log(msg); + reader.release(); // mark reader read + reader.position(reader.position() + tsize + lsize + length); + reader.release(); + return new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + } else { + return frame; + } + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to decode frame", t); + reader.clear(); // mark reader read + return new MalformedFrame(type, Http3Error.H3_INTERNAL_ERROR.code(), t.getMessage(), t); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java new file mode 100644 index 00000000000..858e10fa6a0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; +import java.util.stream.Stream; + +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER; +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_INTEGER_LENGTH; + +/** + * An enum to model HTTP/3 frame types. + */ +public enum Http3FrameType { + + /** + * Used to identify an HTTP/3 frame whose type is unknown + */ + UNKNOWN(-1, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 DATA frame + */ + DATA(TYPE.DATA_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 HEADERS frame + */ + HEADERS(TYPE.HEADERS_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 CANCEL_PUSH frame + */ + CANCEL_PUSH(TYPE.CANCEL_PUSH_FRAME, MAX_INTEGER_LENGTH), + /** + * Used to identify an HTTP/3 SETTINGS frame + */ + SETTINGS(TYPE.SETTINGS_FRAME, TYPE.MAX_SETTINGS_LENGTH), + /** + * Used to identify an HTTP/3 PUSH_PROMISE frame + */ + PUSH_PROMISE(TYPE.PUSH_PROMISE_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 GOAWAY frame + */ + GOAWAY(TYPE.GOAWAY_FRAME, MAX_INTEGER_LENGTH), + /** + * Used to identify an HTTP/3 MAX_PUSH_ID_FRAME frame + */ + MAX_PUSH_ID(TYPE.MAX_PUSH_ID_FRAME, MAX_INTEGER_LENGTH); + + /** + * A class to hold type constants + */ + static final class TYPE { + private TYPE() { throw new InternalError(); } + + // Frames types + public static final int DATA_FRAME = 0x00; + public static final int HEADERS_FRAME = 0x01; + public static final int CANCEL_PUSH_FRAME = 0x03; + public static final int SETTINGS_FRAME = 0x04; + public static final int PUSH_PROMISE_FRAME = 0x05; + public static final int GOAWAY_FRAME = 0x07; + public static final int MAX_PUSH_ID_FRAME = 0x0d; + + // The maximum size a settings frame can have. + // This is a limit imposed by our implementation. + // There are only 7 settings defined in the current + // specification, but we will allow for a frame to + // contain up to 80. Past that limit, we will consider + // the frame to be malformed: + // 8 x 10 x (max sizeof(id) + max sizeof(value)) = 80 x 16 bytes + public static final long MAX_SETTINGS_LENGTH = + 10L * 8L * MAX_INTEGER_LENGTH * 2L; + } + + + // This is one of the values defined in TYPE above, or + // -1 for the UNKNOWN frame types. + private final int type; + private final long maxLength; + private Http3FrameType(int type, long maxLength) { + this.type = type; + this.maxLength = maxLength; + } + + /** + * {@return the frame type, as defined by HTTP/3} + */ + public long type() { return type;} + + /** + * {@return the maximum length a frame of this type + * can take} + */ + public long maxLength() { + return maxLength; + } + + /** + * {@return the HTTP/3 frame type, as an int} + * + * @apiNote + * HTTP/3 defines frames type as variable length integers + * in the range [0, 2^62-1]. However, the few standard frame + * types registered for HTTP/3 and modeled by this enum + * class can be coded as an int. + * This method provides a convenient way to access the frame + * type as an int, which avoids having to cast when using + * the value in switch statements. + */ + public int intType() { return type;} + + /** + * {@return the {@link Http3FrameType} corresponding to the given + * {@code type}, or {@link #UNKNOWN} if no corresponding + * {@link Http3FrameType} instance is found} + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + */ + public static Http3FrameType forType(long type) { + return Stream.of(values()) + .filter(x -> x.type == type) + .findFirst() + .orElse(UNKNOWN); + } + + /** + * {@return a string representation of the given type, suited for inclusion + * in log messages, exceptions, etc...} + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + */ + public static String asString(long type) { + String str = null; + if (type >= Integer.MIN_VALUE && type <= Integer.MAX_VALUE) { + str = switch ((int)type) { + case TYPE.DATA_FRAME -> DATA.name(); // 0x00 + case TYPE.HEADERS_FRAME -> HEADERS.name(); // 0x01 + case 0x02 -> "RESERVED(0x02)"; + case TYPE.CANCEL_PUSH_FRAME -> CANCEL_PUSH.name(); // 0x03 + case TYPE.SETTINGS_FRAME -> SETTINGS.name(); // 0x04 + case TYPE.PUSH_PROMISE_FRAME -> PUSH_PROMISE.name(); // 0x05 + case 0x06 -> "RESERVED(0x06)"; + case TYPE.GOAWAY_FRAME -> GOAWAY.name(); // 0x07 + case 0x08 -> "RESERVED(0x08)"; + case 0x09 -> "RESERVED(0x09)"; + case TYPE.MAX_PUSH_ID_FRAME -> MAX_PUSH_ID.name(); // 0x0d + default -> null; + }; + } + if (str != null) return str; + if (isReservedType(type)) { + return "RESERVED(type=" + type + ")"; + } + return "UNKNOWN(type=" + type + ")"; + } + + /** + * {@return whether this frame type is illegal} + * This corresponds to HTTP/2 frame types that have no equivalent in + * HTTP/3. + * @param type the frame type + */ + public static boolean isIllegalType(long type) { + return type == 0x02 || type == 0x06 || type == 0x08 || type == 0x09; + } + + /** + * Whether the given type is one of the reserved frame + * types defined by HTTP/3. For any non-negative integer N: + * {@code 0x21 + 0x1f * N } + * is a reserved frame type that has no meaning. + * + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + * + * @return true if the given type matches the {@code 0x21 + 0x1f * N} + * pattern + */ + public static boolean isReservedType(long type) { + return type >= 0x21L && type <= MAX_ENCODED_INTEGER + && (type - 0x21L) % 0x1f == 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java new file mode 100644 index 00000000000..0d89666ec26 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.util.function.LongPredicate; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.quic.BuffersReader; + +import jdk.internal.net.http.http3.Http3Error; + +/** + * An instance of MalformedFrame can be returned by + * {@link AbstractHttp3Frame#decode(BuffersReader, LongPredicate, Logger)} + * when a malformed frame is detected. This should cause the caller + * to send an error to its peer, and possibly throw an + * exception to the higher layer. + */ +public class MalformedFrame extends AbstractHttp3Frame { + + private final long errorCode; + private final String msg; + private final Throwable cause; + + /** + * Creates Connection Error malformed frame + * + * @param errorCode - error code + * @param msg - internal debug message + */ + public MalformedFrame(long type, long errorCode, String msg) { + this(type, errorCode, msg, null); + } + + /** + * Creates Connection Error malformed frame + * + * @param errorCode - error code + * @param msg - internal debug message + * @param cause - internal cause for the error, if available + * (can be null) + */ + public MalformedFrame(long type, long errorCode, String msg, Throwable cause) { + super(type); + this.errorCode = errorCode; + this.msg = msg; + this.cause = cause; + } + + @Override + public String toString() { + return super.toString() + " MalformedFrame, Error: " + + Http3Error.stringForCode(errorCode) + + " reason: " + msg; + } + + /** + * {@inheritDoc} + * @implSpec this method always returns 0 + */ + @Override + public long length() { + return 0; // Not Applicable + } + + /** + * {@inheritDoc} + * @implSpec this method always returns 0 + */ + @Override + public long size() { + return 0; // Not applicable + } + + /** + * {@return the {@linkplain Http3Error#code() HTTP/3 error code} that + * should be reported to the peer} + */ + public long getErrorCode() { + return errorCode; + } + + /** + * {@return a message that describe the error} + */ + public String getMessage() { + return msg; + } + + /** + * {@return the cause of the error, if available, {@code null} otherwise} + * + * @apiNote + * This is useful for logging and diagnosis purpose, typically when the + * error is an {@linkplain Http3Error#H3_INTERNAL_ERROR internal error}. + */ + public Throwable getCause() { + return cause; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java new file mode 100644 index 00000000000..b4f35064da1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a MAX_PUSH_ID HTTP3 frame + */ +public final class MaxPushIdFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.MAX_PUSH_ID_FRAME; + + private final long length; + private final long maxPushId; + + public MaxPushIdFrame(final long maxPushId) { + super(Http3FrameType.MAX_PUSH_ID.type()); + this.maxPushId = maxPushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.maxPushId); + } + + // only used when constructing the frame during decoding content over a stream + private MaxPushIdFrame(final long maxPushId, final long length) { + super(Http3FrameType.MAX_PUSH_ID.type()); + this.maxPushId = maxPushId; + this.length = length; + } + + @Override + public long length() { + return this.length; + } + + public long getMaxPushId() { + return this.maxPushId; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the max push id value + VariableLengthEncoder.encode(buf, this.maxPushId); + } + + /** + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * @param reader the reader + * @param debug a logger for debugging purposes + * @return the new frame + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + long position = reader.position(); + decodeRequiredType(reader, TYPE); + long length = VariableLengthEncoder.decode(reader); + if (length > reader.remaining() || length < 0) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed maxPushId + return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in MAX_PUSH_ID frame: " + length); + } + + long maxPushId = VariableLengthEncoder.decode(reader); + if (maxPushId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new MaxPushIdFrame(maxPushId); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java new file mode 100644 index 00000000000..3cbbb814b82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; +import java.util.List; + +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.BuffersReader; + +/** + * A PartialFrame helps to read the payload of a frame. + * This class is not multi-thread safe. + */ +public abstract sealed class PartialFrame + extends AbstractHttp3Frame + permits HeadersFrame, + DataFrame, + PushPromiseFrame, + UnknownFrame { + + private static final List NONE = List.of(); + private final long streamingLength; + private long remaining; + PartialFrame(long frameType, long streamingLength) { + super(frameType); + this.remaining = this.streamingLength = streamingLength; + } + + @Override + public final long streamingLength() { + return streamingLength; + } + + /** + * {@return the number of payload bytes that remains to read} + */ + public final long remaining() { + return remaining; + } + + /** + * Reads remaining payload bytes from the given {@link BuffersReader}. + * This method must not run concurrently with any code that submit + * new buffers to the {@link BuffersReader}. + * @param buffers a {@link BuffersReader} that contains payload bytes. + * @return the payload bytes available so far, an empty list if no + * bytes are available or the whole payload has already been + * read + */ + public final List nextPayloadBytes(BuffersReader buffers) { + var remaining = this.remaining; + if (remaining > 0) { + long available = buffers.remaining(); + if (available > 0) { + long read = Math.min(remaining, available); + this.remaining = remaining - read; + return buffers.getAndRelease(read); + } + } + return NONE; + } + + /** + * Reads remaining payload bytes from the given {@link ByteBuffer}. + * @param buffer a {@link ByteBuffer} that contains payload bytes. + * @return the payload bytes available in the given buffer, or + * {@code null} if all payload has been read. + */ + public final ByteBuffer nextPayloadBytes(ByteBuffer buffer) { + var remaining = this.remaining; + if (remaining > 0) { + int available = buffer.remaining(); + if (available > 0) { + long read = Math.min(remaining, available); + remaining -= read; + this.remaining = remaining; + assert read <= available; + int pos = buffer.position(); + int len = (int) read; + // always create a slice, so that we can move the position + // of the original buffer, as if the data had been read. + ByteBuffer next = buffer.slice(pos, len); + buffer.position(pos + len); + return next; + } else return buffer == QuicStreamReader.EOF ? buffer : buffer.slice(); + } + return null; + } + + /** + * Write the frame headers to the given buffer. + * + * @apiNote + * The caller will be responsible for writing the + * remaining {@linkplain #length() length} bytes of + * the frame content after writing the frame headers. + * + * @implSpec + * Usually the header of a frame is assumed to simply + * contain the frame type and frame length. + * Some subclasses of {@code AbstractHttp3Frame} may + * however include some additional information. + * For instance, {@link PushPromiseFrame} may consider + * the {@link PushPromiseFrame#getPushId() pushId} as + * being in part of the headers, and write it along + * in this method after the frame type and length. + * In such a case, a subclass would also need to + * override {@link #headersSize()} in order to add + * the size of the additional information written + * by {@link #writeHeaders(ByteBuffer)}. + * + * @param buf a buffer to write the headers into + */ + public void writeHeaders(ByteBuffer buf) { + long len = length(); + int pos0 = buf.position(); + VariableLengthEncoder.encode(buf, type()); + VariableLengthEncoder.encode(buf, len); + int pos1 = buf.position(); + assert pos1 - pos0 == super.headersSize(); + } + + @Override + public String toString() { + var len = length(); + return "%s (partial: %s/%s)".formatted(this.getClass().getSimpleName(), len - remaining, len); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java new file mode 100644 index 00000000000..f95fa7f964d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; + +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a PUSH_PROMISE HTTP3 frame + */ +public final class PushPromiseFrame extends PartialFrame { + + /** + * The PUSH_PROMISE frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.PUSH_PROMISE_FRAME; + + private final long length; + private final long pushId; + + public PushPromiseFrame(final long pushId, final long fieldLength) { + super(TYPE, fieldLength); + if (pushId < 0 || pushId > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("invalid pushId: " + pushId); + } + this.pushId = pushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.pushId) + fieldLength; + } + + @Override + public long length() { + return this.length; + } + + public long getPushId() { + return this.pushId; + } + + /** + * Write the frame header and the promise {@link #getPushId() + * pushId} to the given buffer. The caller will be responsible + * for writing the remaining {@link #streamingLength()} bytes + * that constitutes the field section length. + * @param buf a buffer to write the headers into + */ + @Override + public void writeHeaders(ByteBuffer buf) { + super.writeHeaders(buf); + VariableLengthEncoder.encode(buf, this.pushId); + } + + /** + * {@return the number of bytes needed to write the headers and + * the promised {@link #getPushId() pushId}}. + */ + @Override + public int headersSize() { + return super.headersSize() + VariableLengthEncoder.getEncodedSize(pushId); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java new file mode 100644 index 00000000000..90fabd47e68 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER; + +/** + * This class models an HTTP/3 SETTINGS frame + */ +public class SettingsFrame extends AbstractHttp3Frame { + + // An array of setting parameters. + // The index is the parameter id, minus 1, the value is the parameter value + private final long[] parameters; + // HTTP/3 specifies some reserved identifier for which the parameter + // has no semantics and the value is undefined and should be ignored. + // It's excepted that at least one such parameter should be included + // in the settings frame to exercise the fact that undefined parameters + // should be ignored + private long undefinedId; + private long undefinedValue; + + /** + * The SETTINGS frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.SETTINGS_FRAME; + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()) + .append(" Settings: "); + + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + sb.append(name(i+1)) + .append("=") + .append(parameters[i]) + .append(' '); + } + } + if (undefinedId != -1) { + sb.append(name(undefinedId)).append("=") + .append(undefinedValue).append(' '); + } + return sb.toString(); + } + + // TODO: should we use an enum instead? + // HTTP/2 only Parameters - receiving one of those should be + // considered as a protocol error of type SETTINGS_ERROR + public static final int ENABLE_PUSH = 0x2; + public static final int MAX_CONCURRENT_STREAMS = 0x3; + public static final int INITIAL_WINDOW_SIZE = 0x4; + public static final int MAX_FRAME_SIZE = 0x5; + // HTTP/3 Parameters + // This parameter was defined as HEADER_TABLE_SIZE in HTTP/2 + public static final int SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0x1; + public static final int DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0; + // This parameter was defined as MAX_HEADER_LIST_SIZE in HTTP/2 + public static final int SETTINGS_MAX_FIELD_SECTION_SIZE = 0x6; + public static final long DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE = -1; + // Allow compression efficiency by allowing referencing dynamic table entries + // that are still in transit. This parameter specifies the number of streams + // that could become blocked. + public static final int SETTINGS_QPACK_BLOCKED_STREAMS = 0x7; + public static final int DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS = 0; + + public static final int MAX_PARAM = 0x7; + + // maps a parameter id to a parameter name + private String name(long i) { + if (i <= MAX_PARAM) { + return switch ((int)i) { + case SETTINGS_QPACK_MAX_TABLE_CAPACITY -> "SETTINGS_QPACK_MAX_TABLE_CAPACITY"; // 0x01 + case ENABLE_PUSH -> "ENABLE_PUSH"; // 0x02 + case MAX_CONCURRENT_STREAMS -> "MAX_CONCURRENT_STREAMS"; // 0x03 + case INITIAL_WINDOW_SIZE -> "INITIAL_WINDOW_SIZE"; // 0x04 + case MAX_FRAME_SIZE -> "MAX_FRAME_SIZE"; // 0x05 + case SETTINGS_MAX_FIELD_SECTION_SIZE -> "SETTINGS_MAX_FIELD_SECTION_SIZE"; // 0x06 + case SETTINGS_QPACK_BLOCKED_STREAMS -> "SETTINGS_QPACK_BLOCKED_STREAMS"; // 0x07 + default -> "UNKNOWN(0x00)"; // 0x00 ? + }; + } else if (isReservedId(i)) { + return "RESERVED(" + i + ")"; + } else { + return "UNKNOWN(" + i +")"; + } + } + + /** + * Creates a new HTTP/3 SETTINGS frame, including the given + * reserved identifier id and value pair. + * + * @implNote + * We only keep one reserved id/value pair - there's no + * reason to keep more... + * + * @param undefinedId the id of an undefined (reserved) parameter + * @param undefinedValue a random value for the undefined parameter + */ + public SettingsFrame(long undefinedId, long undefinedValue) { + super(TYPE); + parameters = new long [MAX_PARAM]; + Arrays.fill(parameters, -1); + assert undefinedId == -1 || isReservedId(undefinedId); + assert undefinedId != -1 || undefinedValue == -1; + this.undefinedId = undefinedId; + this.undefinedValue = undefinedValue; + } + + /** + * Creates a new empty SETTINGS frame, and allocate a random + * reserved id and value pair. + */ + public SettingsFrame() { + this(nextRandomReservedParameterId(), nextRandomParameterValue()); + } + + /** + * Get the parameter value for the given parameter id + * + * @param paramID the parameter id + * + * @return the value of the given parameter, if present, + * {@code -1}, if absent + * + * @throws IllegalArgumentException if the parameter id is negative or + * {@linkplain #isIllegal(long) illegal} + * + */ + public synchronized long getParameter(int paramID) { + if (isIllegal(paramID)) { + throw new IllegalArgumentException("illegal parameter: " + paramID); + } + if (undefinedId != -1 && paramID == undefinedId) + return undefinedValue; + if (paramID > MAX_PARAM) return -1; + return parameters[paramID - 1]; + } + + /** + * Sets the given parameter to the given value. + * + * @param paramID the parameter id + * @param value the parameter value + * + * @return this + * + * @throws IllegalArgumentException if the parameter id is negative or + * {@linkplain #isIllegal(long) illegal} + */ + public synchronized SettingsFrame setParameter(long paramID, long value) { + // subclasses can override this to actually send + // an illegal parameter + if (isIllegal(paramID) || paramID < 1 || paramID > MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("illegal parameter: " + paramID); + } + if (paramID <= MAX_PARAM) { + parameters[(int)paramID - 1] = value; + } else if (isReservedId(paramID)) { + this.undefinedId = paramID; + this.undefinedValue = value; + } + return this; + } + + @Override + public long length() { + int len = 0; + int i = 0; + for (long p : parameters) { + if (p != -1) { + len += VariableLengthEncoder.getEncodedSize(i+1); + len += VariableLengthEncoder.getEncodedSize(p); + } + } + if (undefinedId != -1) { + assert isReservedId(undefinedId); + len += VariableLengthEncoder.getEncodedSize(undefinedId); + len += VariableLengthEncoder.getEncodedSize(undefinedValue); + } + return len; + } + + /** + * Writes this frame to the given buffer. + * + * @param buf a byte buffer to write this frame into + * + * @throws java.nio.BufferUnderflowException if the buffer + * doesn't have enough space + */ + public void writeFrame(ByteBuffer buf) { + long size = size(); + long len = length(); + int pos0 = buf.position(); + VariableLengthEncoder.encode(buf, TYPE); + VariableLengthEncoder.encode(buf, len); + int pos1 = buf.position(); + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + VariableLengthEncoder.encode(buf, i+1); + VariableLengthEncoder.encode(buf, parameters[i]); + } + } + if (undefinedId != -1) { + // Setting identifiers of the format 0x1f * N + 0x21 for + // non-negative integer values of N are reserved to exercise + // the requirement that unknown identifiers be ignored. + // Such settings have no defined meaning. Endpoints SHOULD + // include at least one such setting in their SETTINGS frame + assert isReservedId(undefinedId); + VariableLengthEncoder.encode(buf, undefinedId); + VariableLengthEncoder.encode(buf, undefinedValue); + } + assert buf.position() - pos1 == len; + assert buf.position() == pos0 + size; + } + + /** + * Decodes a SETTINGS frame from the given reader. + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * + * @param reader a reader containing bytes + * + * @return a new SettingsFrame frame, or a MalformedFrame. + * + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + public static AbstractHttp3Frame decodeFrame(BuffersReader reader, Logger debug) { + final long pos = reader.position(); + decodeRequiredType(reader, TYPE); + final SettingsFrame frame = new SettingsFrame(-1, -1); + long length = VariableLengthEncoder.decode(reader); + + // is that OK? Find what's the actual limit for + // a frame length... + if (length > reader.remaining()) { + reader.position(pos); + throw new BufferUnderflowException(); + } + + // position before reading payload + long start = reader.position(); + + while (length > reader.position() - start) { + long id = VariableLengthEncoder.decode(reader); + long value = VariableLengthEncoder.decode(reader); + if (id == -1 || value == -1) { + return new MalformedFrame(TYPE, + Http3Error.H3_FRAME_ERROR.code(), + "Invalid SETTINGS frame contents."); + } + try { + frame.setParameter(id, value); + } catch (IllegalArgumentException iae) { + String msg = "H3_SETTINGS_ERROR: " + iae.getMessage(); + if (debug.on()) debug.log(msg, iae); + reader.position(start + length); + reader.release(); + return new MalformedFrame(TYPE, + Http3Error.H3_SETTINGS_ERROR.code(), + iae.getMessage(), + iae); + } + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return frame; + } + + public static SettingsFrame defaultRFCSettings() { + SettingsFrame f = new SettingsFrame() + .setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, + DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE) + .setParameter(SETTINGS_QPACK_MAX_TABLE_CAPACITY, + DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY) + .setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS); + return f; + } + + public boolean isIllegal(long parameterId) { + // Parameters with 0x0, 0x2, 0x3, 0x4 and 0x5 ids are reserved, + // 0x6 is the legal one: + // https://www.rfc-editor.org/rfc/rfc9114.html#name-settings-parameters + // 0x1 and 0x7 defined by QPACK as a legal one: + // https://www.rfc-editor.org/rfc/rfc9204.html#name-configuration + return parameterId < SETTINGS_MAX_FIELD_SECTION_SIZE && + parameterId != SETTINGS_QPACK_MAX_TABLE_CAPACITY; + } + + public static long nextRandomParameterValue() { + long value = RANDOM.nextLong(0, MAX_ENCODED_INTEGER + 1); + assert value >= 0 && value <= MAX_ENCODED_INTEGER; + return value; + } + + private static final long MAX_N = (MAX_ENCODED_INTEGER - 0x21L) / 0x1fL; + public static long nextRandomReservedParameterId() { + long N = RANDOM.nextLong(0, MAX_N + 1); + long id = 0x1fL * N + 0x21L; + assert id <= MAX_ENCODED_INTEGER; + assert id >= 0x21L; + assert isReservedId(id) : "generated id is not undefined: " + id; + return id; + } + + /** + * Tells whether the given id is one of the undefined parameter ids that + * are reserved and have no meaning. + * + * @apiNote + * Setting identifiers of the format 0x1f * N + 0x21 + * for non-negative integer values of N are reserved to + * exercise the requirement that unknown identifiers be + * ignored + * + * @param id the parameter id + * + * @return true if this is one of the reserved identifiers + */ + public static boolean isReservedId(long id) { + return id >= 0x21 && id < MAX_ENCODED_INTEGER && (id - 0x21) % 0x1f == 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java new file mode 100644 index 00000000000..4426493ed7c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +/** + * A class to model an unknown or reserved frame. + * @apiNote + * From RFC 9114: + *
    + * Frame types of the format 0x1f * N + 0x21 for non-negative integer + * values of N are reserved to exercise the requirement that + * unknown types be ignored (Section 9). These frames have no semantics, + * and MAY be sent on any stream where frames are allowed to be sent. + * This enables their use for application-layer padding. Endpoints MUST NOT + * consider these frames to have any meaning upon receipt. + *
    + * + * @apiNote + * An instance of {@code UnknownFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see jdk.internal.net.http.http3.frames.PartialFrame + * */ +public final class UnknownFrame extends PartialFrame { + final long length; + UnknownFrame(long type, long length) { + super(type, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + + /** + * {@return true if this frame type is one of the reserved + * types} + */ + public boolean isReserved() { + return Http3FrameType.isReservedType(type); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java new file mode 100644 index 00000000000..8399a4550ff --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.streams; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; + +public final class Http3Streams { + public static final int CONTROL_STREAM_CODE = 0x00; + public static final int PUSH_STREAM_CODE = 0x01; + public static final int QPACK_ENCODER_STREAM_CODE = 0x02; + public static final int QPACK_DECODER_STREAM_CODE = 0x03; + + private Http3Streams() { throw new InternalError(); } + + public enum StreamType { + CONTROL(CONTROL_STREAM_CODE), + PUSH(PUSH_STREAM_CODE), + QPACK_ENCODER(QPACK_ENCODER_STREAM_CODE), + QPACK_DECODER(QPACK_DECODER_STREAM_CODE); + final int code; + StreamType(int code) { + this.code = code; + } + public final int code() { + return code; + } + public static Optional ofCode(long code) { + return EnumSet.allOf(StreamType.class).stream() + .filter(s -> s.code() == code) + .findFirst(); + } + } + + /** + * {@return an optional string that represents the error state of the + * stream, or {@code Optional.empty()} if no error code + * has been received or sent} + * @param stream a quic stream that may have errors + */ + public static Optional errorCodeAsString(QuicStream stream) { + long sndErrorCode = -1; + long rcvErrorCode = -1; + if (stream instanceof QuicReceiverStream rcv) { + rcvErrorCode = rcv.rcvErrorCode(); + } + if (stream instanceof QuicSenderStream snd) { + sndErrorCode = snd.sndErrorCode(); + } + if (rcvErrorCode >= 0 || sndErrorCode >= 0) { + Stream rcv = rcvErrorCode >= 0 + ? Stream.of("RCV: " + Http3Error.stringForCode(rcvErrorCode)) + : Stream.empty(); + Stream snd = sndErrorCode >= 0 + ? Stream.of("SND: " + Http3Error.stringForCode(sndErrorCode)) + : Stream.empty(); + return Optional.of(Stream.concat(rcv, snd) + .collect(Collectors.joining(",", "errorCode(", ")" ))); + } + return Optional.empty(); + } + + /** + * If the stream has errors, prints a message recording the + * {@linkplain #errorCodeAsString(QuicStream) error state} of the + * stream through the given logger. The message is of the form: + * {@code : }. + * If the given {@code name} is null or empty, {@code "Stream"} is substituted + * to {@code }. + * @param logger the logger to log through + * @param stream a quic stream that may have errors + * @param name a name for the stream, e.g {@code "Control stream"}, or {@code null}. + */ + public static void debugErrorCode(Logger logger, QuicStream stream, String name) { + if (logger.on()) { + var errorCodeStr = errorCodeAsString(stream); + if (errorCodeStr.isPresent()) { + var what = (name == null || name.isEmpty()) ? "Stream" : name; + logger.log("%s %s: %s", what, stream.streamId(), errorCodeStr.get()); + } + } + } + + public static boolean isReserved(long streamType) { + return streamType % 31 == 2 && streamType > 31; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java new file mode 100644 index 00000000000..5db767902f9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; + +/** + * A class that analyzes the first byte of the stream to figure + * out where to dispatch it. + */ +public abstract class PeerUniStreamDispatcher { + private final QuicStreamIntReader reader; + private final QuicReceiverStream stream; + private final CompletableFuture cf = new MinimalFuture<>(); + + /** + * Creates a {@code PeerUniStreamDispatcher} for the given stream. + * @param stream a new unidirectional stream opened by the peer + */ + protected PeerUniStreamDispatcher(QuicReceiverStream stream) { + this.reader = new QuicStreamIntReader(checkStream(stream), debug()); + this.stream = stream; + } + + private static QuicReceiverStream checkStream(QuicReceiverStream stream) { + if (!stream.isRemoteInitiated()) { + throw new IllegalArgumentException("stream " + stream.streamId() + " is not peer initiated"); + } + if (stream.isBidirectional()) { + throw new IllegalArgumentException("stream " + stream.streamId() + " is not unidirectional"); + } + return stream; + } + + /** + * {@return a completable future that will contain the dispatched stream, + * once dispatched, or a throwable if dispatching the stream failed} + */ + public CompletableFuture dispatchCF() { + return cf; + } + + // The dispatch function. + private void dispatch(Long result, Throwable error) { + if (result != null && result == Http3Streams.PUSH_STREAM_CODE) { + reader.readInt().whenComplete(this::dispatchPushStream); + return; + } + reader.stop(); + if (result != null) { + cf.complete(stream); + if (Http3Streams.isReserved(result)) { + // reserved stream type, 0x1f * N + 0x21 + reservedStreamType(result, stream); + return; + } + if (result < 0) { + debug().log("stream %s EOF, cannot dispatch!", + stream.streamId()); + abandon(); + } + if (result > Integer.MAX_VALUE) { + unknownStreamType(result, stream); + return; + } + int code = (int)(long)result; + switch (code) { + case Http3Streams.CONTROL_STREAM_CODE -> { + controlStream("peer control stream", StreamType.CONTROL); + } + case Http3Streams.QPACK_ENCODER_STREAM_CODE -> { + qpackEncoderStream("peer qpack encoder stream", StreamType.QPACK_ENCODER); + } + case Http3Streams.QPACK_DECODER_STREAM_CODE -> { + qpackDecoderStream("peer qpack decoder stream", StreamType.QPACK_DECODER); + } + default -> { + unknownStreamType(code, stream); + } + } + } else if (error instanceof IOException io) { + if (stream.receivingState().isReset()) { + debug().log("stream %s %s before stream type received, cannot dispatch!", + stream.streamId(), stream.receivingState()); + // RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-10 + // > A receiver MUST tolerate unidirectional streams being closed or reset + // > prior to the reception of the unidirectional stream header + cf.complete(stream); + abandon(); + return; + } + abort(io); + } else { + // We shouldn't come here, so if we do, it's closer to an + // internal error than a stream creation error. + abort(error); + } + } + + private void dispatchPushStream(Long result, Throwable error) { + reader.stop(); + if (result != null) { + cf.complete(stream); + if (result < 0) { + debug().log("stream %s EOF, cannot dispatch!", + stream.streamId()); + abandon(); + } else { + pushStream("push stream", StreamType.PUSH, result); + } + } else if (error instanceof IOException io) { + if (stream.receivingState().isReset()) { + debug().log("stream %s %s before push stream ID received, cannot dispatch!", + stream.streamId(), stream.receivingState()); + // RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-10 + // > A receiver MUST tolerate unidirectional streams being closed or reset + // > prior to the reception of the unidirectional stream header + cf.complete(stream); + abandon(); + return; + } + abort(io); + } else { + // We shouldn't come here, so if we do, it's closer to an + // internal error than a stream creation error. + abort(error); + } + + } + + // dispatches the peer control stream + private void controlStream(String description, StreamType type) { + assert type.code() == Http3Streams.CONTROL_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onControlStreamCreated(description, stream); + } + + // dispatches the peer encoder stream + private void qpackEncoderStream(String description, StreamType type) { + assert type.code() == Http3Streams.QPACK_ENCODER_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onEncoderStreamCreated(description, stream); + } + + // dispatches the peer decoder stream + private void qpackDecoderStream(String description, StreamType type) { + assert type.code() == Http3Streams.QPACK_DECODER_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onDecoderStreamCreated(description, stream); + } + + // dispatches a push stream initiated by the peer + private void pushStream(String description, StreamType type, long pushId) { + assert type.code() == Http3Streams.PUSH_STREAM_CODE; + debug().log("dispatching %s %s(%s, %s)", description, type, type.code(), pushId); + onPushStreamCreated(description, stream, pushId); + } + + // dispatches a stream whose stream type was recognized as a reserved stream type + private void reservedStreamType(long code, QuicReceiverStream stream) { + onReservedStreamType(code, stream); + } + + // dispatches a stream whose stream type was not recognized + private void unknownStreamType(long code, QuicReceiverStream stream) { + onUnknownStreamType(code, stream); + // if an exception is thrown above, abort will be called. + } + + /** + * {@return the debug logger that should be used} + */ + protected abstract Logger debug(); + + /** + * Starts the dispatcher. + * @apiNote + * The dispatcher should be explicitly started after + * creating the dispatcher. + */ + protected void start() { + reader.readInt().whenComplete(this::dispatch); + } + + /** + * This method disconnects the reader, stops the dispatch, and unless + * the stream type could be decoded and was a {@linkplain Http3Streams#isReserved(long) + * reserved type}, calls {@link #onStreamAbandoned(QuicReceiverStream)} + */ + protected void abandon() { + onStreamAbandoned(stream); + } + + /** + * Aborts the dispatch - for instance, if the stream type + * can't be read, or isn't recognized. + *

    + * This method requests the peer to stop sending this stream, + * and completes the {@link #dispatchCF() dispatchCF} exceptionally + * with the provided throwable. + * + * @param throwable the reason for aborting the dispatch + */ + private void abort(Throwable throwable) { + try { + var debug = debug(); + if (debug.on()) debug.log("aborting dispatch: " + throwable, throwable); + if (!stream.receivingState().isReset() && !stream.isStopSendingRequested()) { + stream.requestStopSending(Http3Error.H3_INTERNAL_ERROR.code()); + } + } finally { + abandon(); + cf.completeExceptionally(throwable); + } + } + + /** + * Called when a reserved stream type is read off the + * stream. + * + * @implSpec + * The default implementation of this method calls + * {@snippet : + * stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + * } + * + * @param code the unrecognized stream type + * @param stream the peer initiated stream + */ + protected void onReservedStreamType(long code, QuicReceiverStream stream) { + debug().log("Ignoring reserved stream type %s", code); + stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + } + + /** + * Called when an unrecognized stream type is read off the + * stream. + * + * @implSpec + * The default implementation of this method calls + * {@snippet : + * stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + * abandon(); + * } + * + * @param code the unrecognized stream type + * @param stream the peer initiated stream + */ + protected void onUnknownStreamType(long code, QuicReceiverStream stream) { + debug().log("Ignoring unknown stream type %s", code); + stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + abandon(); + } + + /** + * Called after disconnecting to abandon a peer initiated stream. + * @param stream a peer initiated stream which was abandoned due to having an + * unknown type, or which was abandoned due to being reset + * before being dispatched. + * @apiNote + * A subclass may want to override this method in order to, e.g, emit a + * QPack Stream Cancellation instruction; + * See https://www.rfc-editor.org/rfc/rfc9204.html#name-abandonment-of-a-stream + */ + protected void onStreamAbandoned(QuicReceiverStream stream) {} + + /** + * Called after disconnecting to handle the peer control stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer control stream + */ + protected abstract void onControlStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle the peer encoder stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer encoder stream + */ + protected abstract void onEncoderStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle the peer decoder stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer decoder stream + */ + protected abstract void onDecoderStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle a peer initiated push stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream a peer initiated push stream + */ + protected abstract void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java new file mode 100644 index 00000000000..bff353a648a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * A class that models a pair of unidirectional streams, where + * data to be written is simply submitted to a queue. + */ +public class QueuingStreamPair extends UniStreamPair { + + // a queue of ByteBuffers submitted for writing. + protected final ConcurrentLinkedQueue writerQueue; + + /** + * Creates a new {@code QueuingStreamPair} for the given HTTP/3 {@code streamType}. + * Valid values for {@code streamType} are {@link StreamType#CONTROL}, + * {@link StreamType#QPACK_ENCODER}, and {@link StreamType#QPACK_DECODER}. + *

    + * This class implements a read loop and a write loop. + *

    + * The read loop will call the given {@code receiver} + * whenever a {@code ByteBuffer} is received. + *

    + * Data can be written to the stream simply by {@linkplain + * #submitData(ByteBuffer) submitting} it to the + * internal unbounded queue managed by this stream. + * When the stream becomes writable, the write loop is invoked and all + * pending data in the queue is written to the stream, until the stream + * is blocked or the queue is empty. + * + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param errorHandler the error handler invoked in case of read errors + * @param logger the debug logger + */ + public QueuingStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + StreamErrorHandler errorHandler, + Logger logger) { + // initialize writer queue before the parent constructor starts the writer loop + writerQueue = new ConcurrentLinkedQueue<>(); + super(streamType, quicConnection, receiver, errorHandler, logger); + } + + /** + * {@return the available credit, taking into account data that has + * not been submitted yet} + * This is only weakly consistent. + */ + public long credit() { + var writer = localWriter(); + long credit = (writer == null) ? 0 : writer.credit(); + if (writerQueue.isEmpty()) return credit; + return credit - writerQueue.stream().mapToLong(Buffer::remaining).sum(); + } + + /** + * Submit data to be written to the sending stream via this + * object's internal queue. + * @param buffer the data to submit + */ + public final void submitData(ByteBuffer buffer) { + writerQueue.offer(buffer); + localWriteScheduler().runOrSchedule(); + } + + // The local control stream write loop + @Override + void localWriterLoop() { + var writer = localWriter(); + if (writer == null) return; + assert !(writer instanceof QueuingWriter); + ByteBuffer buffer; + if (debug.on()) + debug.log("start control writing loop: credit=" + writer.credit()); + while (writer.credit() > 0 && (buffer = writerQueue.poll()) != null) { + try { + if (debug.on()) + debug.log("schedule %s bytes for writing on control stream", buffer.remaining()); + writer.scheduleForWriting(buffer, buffer == QuicStreamReader.EOF); + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to write to control stream", t); + } + errorHandler.onError(writer.stream(), this, t); + } + } + } + + @Override + QuicStreamWriter wrap(QuicStreamWriter writer) { + return new QueuingWriter(writer); + } + + /** + * A class that wraps the actual {@code QuicStreamWriter} + * and redirect everything to the QueuingStreamPair's + * writerQueue - so that data is not sent out of order. + */ + class QueuingWriter extends QuicStreamWriter { + final QuicStreamWriter writer; + QueuingWriter(QuicStreamWriter writer) { + super(QueuingStreamPair.this.localWriteScheduler()); + this.writer = writer; + } + + @Override + public SendingStreamState sendingState() { + return writer.sendingState(); + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + if (!last || buffer.hasRemaining()) submitData(buffer); + if (last) submitData(QuicStreamReader.EOF); + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + QueuingStreamPair.this.writerQueue.offer(buffer); + } + + @Override + public long credit() { + return QueuingStreamPair.this.credit(); + } + + @Override + public void reset(long errorCode) throws IOException { + writer.reset(errorCode); + } + + @Override + public QuicSenderStream stream() { + return writer.stream(); + } + + @Override + public boolean connected() { + return writer.connected(); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java new file mode 100644 index 00000000000..a9101b48ad9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.streams; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; + +/** + * A class that reads VL integers from a QUIC stream. + *

    + * After constructing an instance of this class, the application + * is can call {@link #readInt()} to read one VL integer off the stream. + * When the read operation completes, the application can call {@code readInt} + * again, or call {@link #stop()} to disconnect the reader. + */ +final class QuicStreamIntReader { + private final SequentialScheduler scheduler = SequentialScheduler.lockingScheduler(this::dispatch); + private final QuicReceiverStream stream; + private final QuicStreamReader reader; + private final Logger debug; + private CompletableFuture cf; + private ByteBuffer vlongBuf; // accumulate bytes until stream type can be decoded + + /** + * Creates a {@code QuicStreamIntReader} for the given stream. + * @param stream a receiver stream with no connected reader + * @param debug a logger + */ + public QuicStreamIntReader(QuicReceiverStream stream, Logger debug) { + this.stream = stream; + this.reader = stream.connectReader(scheduler); + this.debug = debug; + debug.log("int reader created for stream " + stream.streamId()); + } + + // The read loop. Attempts to read a VL int, and completes the CF when done. + private void dispatch() { + if (cf == null) return; // not reading anything at the moment + try { + ByteBuffer buffer; + while ((buffer = reader.peek()) != null) { + if (buffer == QuicStreamReader.EOF) { + debug.log("stream %s EOF, cannot complete!", + stream.streamId()); + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(-1L); + return; + } + if (buffer.remaining() == 0) { + var polled = reader.poll(); + assert buffer == polled; + continue; + } + if (vlongBuf == null) { + long vlong = VariableLengthEncoder.decode(buffer); + if (vlong >= 0) { + // happy case: we have enough bytes in the buffer + if (buffer.remaining() == 0) { + var polled = reader.poll(); + assert buffer == polled; + } + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(vlong); + return; + } + // we don't have enough bytes: start accumulating them + int vlongSize = VariableLengthEncoder.peekEncodedValueSize(buffer, buffer.position()); + assert vlongSize > 0 && vlongSize <= VariableLengthEncoder.MAX_INTEGER_LENGTH + : vlongSize + " is out of bound for a variable integer size (should be in [1..8]"; + assert buffer.remaining() < vlongSize; + vlongBuf = ByteBuffer.allocate(vlongSize); + vlongBuf.put(buffer); + assert buffer.remaining() == 0; + var polled = reader.poll(); + assert polled == buffer; + // continue and wait for more + } else { + // there wasn't enough bytes the first time around, accumulate + // missing bytes + int missing = vlongBuf.remaining(); + int available = Math.min(missing, buffer.remaining()); + for (int i = 0; i < available; i++) { + vlongBuf.put(buffer.get()); + } + // if we have exhausted the buffer, poll it. + if (!buffer.hasRemaining()) { + var polled = reader.poll(); + assert polled == buffer; + } + // if we have all bytes, we can proceed and decode the stream type + if (!vlongBuf.hasRemaining()) { + vlongBuf.flip(); + long vlong = VariableLengthEncoder.decode(vlongBuf); + assert !vlongBuf.hasRemaining(); + vlongBuf = null; + assert vlong >= 0; + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(vlong); + return; + } // otherwise, wait for more + } + } + } catch (Throwable throwable) { + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.completeExceptionally(throwable); + } + } + + /** + * Stops and disconnects this reader. This operation must not be done when a read operation + * is in progress. If cancelling a read operation is intended, use + * {@link QuicReceiverStream#requestStopSending(long)}. + * @throws IllegalStateException if a read operation is currently in progress. + */ + public synchronized void stop() { + if (cf != null) { + // if a read is in progress, some bytes might have been read + // off the stream already, and stopping the reader could corrupt the data. + throw new IllegalStateException("Reading in progress"); + } + if (!reader.connected()) return; + stream.disconnectReader(reader); + scheduler.stop(); + } + + /** + * Starts a read operation to decode a single number. + * @return a {@link CompletableFuture} that will be completed + * with the decoded number, or -1 if the stream is terminated before + * the complete number could be read, or an exception + * if the stream is reset or decoding fails. + * @throws IllegalStateException if the reader is stopped, or if a read + * operation is already in progress + */ + public synchronized CompletableFuture readInt() { + if (cf != null) { + throw new IllegalStateException("Read in progress"); + } + if (!reader.connected()) { + throw new IllegalStateException("Reader stopped"); + } + var cf0 = cf = new MinimalFuture<>(); + reader.start(); + scheduler.runOrSchedule(); + return cf0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java new file mode 100644 index 00000000000..403b26f244d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import jdk.internal.net.http.Http3Connection; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; + +/** + * A class that models a pair of HTTP/3 unidirectional streams. + * This class implements a read loop that calls a {@link + * #UniStreamPair(StreamType, QuicConnection, Consumer, Runnable, StreamErrorHandler, Logger) + * receiver} every time a {@code ByteBuffer} is read from + * the receiver part. + * The {@linkplain #futureSenderStreamWriter() sender stream writer}, + * when available, can be used to write to the sender part. + * The {@link #UniStreamPair(StreamType, QuicConnection, Consumer, Runnable,StreamErrorHandler, Logger) + * writerLoop} is invoked whenever the writer part becomes unblocked, and + * writing can be resumed. + *

    + * @apiNote + * The creator of the stream pair (typically {@link Http3Connection}) is expected + * to complete the {@link #futureReceiverStream()} completable future when the remote + * part of the stream pair is created by the remote peer. This class will not + * listen directly for creation of new remote streams. + *

    + * The {@link QueuingStreamPair} class is a subclass of this class which + * implements a writer loop over an unbounded queue of {@code ByteBuffer}, and + * can be used when unlimited buffering of data for writing is not an issue. + */ +public class UniStreamPair { + + // The sequential scheduler for the local control stream (LCS) writer loop + private final SequentialScheduler localWriteScheduler; + // The QuicStreamWriter for the local control stream + private volatile QuicStreamWriter localWriter; + private final CompletableFuture streamWriterCF; + // A completable future that will be completed when the local sender + // stream is opened and the stream type has been queued to the + // writer queue. + private volatile CompletableFuture localSenderStreamCF; + + // The sequential scheduler for the peer receiver stream (PRS) reader loop + final SequentialScheduler peerReadScheduler = + SequentialScheduler.lockingScheduler(this::peerReaderLoop); + // The QuicStreamReader for the peer control stream + volatile QuicStreamReader peerReader; + private final CompletableFuture streamReaderCF; + // A completable future that will be completed when the peer opens + // the receiver part of the stream pair + private final CompletableFuture peerReceiverStreamCF = new MinimalFuture<>(); + private final ReentrantLock lock = new ReentrantLock(); + + + private final StreamType localStreamType; // The HTTP/3 stream type of the sender part + private final StreamType remoteStreamType; // The HTTP/3 stream type of the receiver part + private final QuicConnection quicConnection; // the underlying quic connection + private final Consumer receiver; // called when a ByteBuffer is received + final StreamErrorHandler errorHandler; // used by QueuingStreamPair + final Logger debug; // the debug logger + + /** + * Creates a new {@code UniStreamPair} for the given HTTP/3 {@code streamType}. + * Valid values for {@code streamType} are {@link StreamType#CONTROL}, + * {@link StreamType#QPACK_ENCODER}, and {@link StreamType#QPACK_DECODER}. + *

    + * This class implements a read loop that will call the given {@code receiver} + * whenever a {@code ByteBuffer} is received. + *

    + * Writing to the sender part can be done by interacting directly with + * the writer. If the writer is blocked due to flow control, and becomes + * unblocked again, the {@code writeLoop} is invoked. + * The {@link QueuingStreamPair} subclass provides a convenient implementation + * of a {@code writeLoop} based on an unbounded queue of {@code ByteBuffer}. + * + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param writerLoop the writer loop + * @param errorHandler the error handler invoked in case of read errors + * @param logger the debug logger + */ + public UniStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + Runnable writerLoop, + StreamErrorHandler errorHandler, + Logger logger) { + this(local(streamType), remote(streamType), + Objects.requireNonNull(quicConnection), + Objects.requireNonNull(receiver), + Optional.of(writerLoop), + Objects.requireNonNull(errorHandler), + Objects.requireNonNull(logger)); + } + + /** + * A constructor used by the {@link QueuingStreamPair} subclass + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param errorHandler the error handler invoked in case + * of read or write errors + * @param logger + */ + UniStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + StreamErrorHandler errorHandler, + Logger logger) { + this(local(streamType), remote(streamType), + Objects.requireNonNull(quicConnection), + Objects.requireNonNull(receiver), + Optional.empty(), + errorHandler, + Objects.requireNonNull(logger)); + } + + // all constructors delegate here + private UniStreamPair(StreamType localStreamType, + StreamType remoteStreamType, + QuicConnection quicConnection, + Consumer receiver, + Optional writerLoop, + StreamErrorHandler errorHandler, + Logger logger) { + assert this.getClass() != UniStreamPair.class + || writerLoop.isPresent(); + this.debug = logger; + this.localStreamType = localStreamType; + this.remoteStreamType = remoteStreamType; + this.quicConnection = quicConnection; + this.receiver = receiver; + this.errorHandler = errorHandler; + var localWriterLoop = writerLoop.orElse(this::localWriterLoop); + this.localWriteScheduler = + SequentialScheduler.lockingScheduler(localWriterLoop); + this.streamWriterCF = startSending(); + this.streamReaderCF = startReceiving(); + } + + private static StreamType local(StreamType localStreamType) { + return switch (localStreamType) { + case CONTROL -> localStreamType; + case QPACK_ENCODER -> localStreamType; + case QPACK_DECODER -> localStreamType; + default -> throw new IllegalArgumentException(localStreamType + + " cannot be part of a stream pair"); + }; + } + + private static StreamType remote(StreamType localStreamType) { + return switch (localStreamType) { + case CONTROL -> localStreamType; + case QPACK_ENCODER -> StreamType.QPACK_DECODER; + case QPACK_DECODER -> StreamType.QPACK_ENCODER; + default -> throw new IllegalArgumentException(localStreamType + + " cannot be part of a stream pair"); + }; + } + + /** + * {@return the HTTP/3 stream type of the sender part of the stream pair} + */ + public final StreamType localStreamType() { + return localStreamType; + } + + /** + * {@return the HTTP/3 stream type of the receiver part of the stream pair} + */ + public final StreamType remoteStreamType() { + return remoteStreamType; + } + + /** + * {@return a completable future that will be completed with a writer connected + * to the sender part of this stream pair after the local HTTP/3 stream type + * has been queued for writing on the writing queue} + */ + public final CompletableFuture futureSenderStreamWriter() { + return streamWriterCF; + } + + /** + * {@return a completable future that will be completed with a reader connected + * to the receiver part of this stream pair after the remote HTTP/3 stream + * type has been read off the remote initiated stream} + */ + public final CompletableFuture futureReceiverStreamReader() { + return streamReaderCF; + } + + /** + * {@return a completable future that will be completed with the sender part + * of this stream pair after the local HTTP/3 stream type + * has been queued for writing on the writing queue} + */ + public CompletableFuture futureSenderStream() { + return localSenderStream(); + } + + /** + * {@return a completable future that will be completed with the receiver part + * of this stream pair after the remote HTTP/3 stream type has been read off + * the remote initiated stream} + */ + public CompletableFuture futureReceiverStream() { + return peerReceiverStreamCF; + } + + /** + * {@return the scheduler for the local writer loop} + */ + public SequentialScheduler localWriteScheduler() { + return localWriteScheduler; + } + + /** + * {@return the writer connected to the sender part of this stream or + * {@code null} if no writer is connected yet} + */ + public QuicStreamWriter localWriter() {return localWriter; } + + /** + * Stops schedulers. Can be called when the connection is + * closed to stop the reading and writing loops. + */ + public void stopSchedulers() { + peerReadScheduler.stop(); + localWriteScheduler.stop(); + } + + // Hooks for QueuingStreamPair + // ============================ + + /** + * This method is overridden by {@link QueuingStreamPair} to implement + * a writer loop for this stream. It is only called when the concrete + * subclass is {@link QueuingStreamPair}. + */ + void localWriterLoop() { + if (debug.on()) debug.log("writing loop not implemented"); + } + + + /** + * Used by subclasses to redirect queuing of data to the + * subclass queue. + * @param writer the downstream writer + * @return a writer that can be safely used. + */ + QuicStreamWriter wrap(QuicStreamWriter writer) { + return writer; + } + + // Undidirectional Stream Pair Implementation + // ========================================== + + + /** + * This method is called to process bytes received on the peer + * control stream. + * @param buffer the bytes received + */ + private void processPeerControlBytes(ByteBuffer buffer) { + receiver.accept(buffer); + } + + /** + * Creates the local sender stream and queues the stream + * type code in its writer queue. + * @return a completable future that will be completed with the + * local sender stream + */ + private CompletableFuture localSenderStream() { + CompletableFuture lcs = localSenderStreamCF; + if (lcs != null) return lcs; + StreamType type = localStreamType(); + lock.lock(); + try { + if ((lcs = localSenderStreamCF) != null) return lcs; + if (debug.on()) { + debug.log("Opening local stream: %s(%s)", + type, type.code()); + } + // TODO: review this duration + final Duration streamLimitIncreaseDuration = Duration.ZERO; + localSenderStreamCF = lcs = quicConnection + .openNewLocalUniStream(streamLimitIncreaseDuration) + .thenApply( s -> openLocalStream(s, type.code())); + // TODO: use thenApplyAsync with the executor instead + } finally { + lock.unlock(); + } + return lcs; + } + + + /** + * Schedules sending of client settings. + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + private CompletableFuture startSending() { + return localSenderStream().thenApply((stream) -> { + if (debug.on()) { + debug.log("stream %s is ready for sending", stream.streamId()); + } + var controlWriter = stream.connectWriter(localWriteScheduler); + localWriter = controlWriter; + localWriteScheduler.runOrSchedule(); + return wrap(controlWriter); + }); + } + + /** + * Schedules the receiving of server settings + * @return a completable future that will be completed with the + * {@link QuicStreamReader} allowing to read from the remote control + * stream. + */ + private CompletableFuture startReceiving() { + if (debug.on()) { + debug.log("prepare to receive"); + } + return peerReceiverStreamCF.thenApply(this::connectReceiverStream); + } + + /** + * Connects the peer control stream reader and + * schedules the receiving of the peer settings from the given + * {@code peerControlStream}. + * @param peerControlStream the peer control stream + * @return the peer control stream reader + */ + private QuicStreamReader connectReceiverStream(QuicReceiverStream peerControlStream) { + var reader = peerControlStream.connectReader(peerReadScheduler); + var streamType = remoteStreamType(); + if (debug.on()) { + debug.log("peer %s stream reader connected (stream %s)", + streamType, peerControlStream.streamId()); + } + peerReader = reader; + reader.start(); + return reader; + } + + // The peer receiver stream reader loop + private void peerReaderLoop() { + var reader = peerReader; + if (reader == null) return; + ByteBuffer buffer; + long bytes = 0; + var streamType = remoteStreamType(); + try { + // TODO: Revisit: if the underlying quic connection is closed + // by the peer, we might get a ClosedChannelException from poll() + // here before the upper layer connection (HTTP/3 connection) is + // marked closed. + if (debug.on()) { + debug.log("start reading from peer %s stream", streamType); + } + while ((buffer = reader.poll()) != null) { + final int remaining = buffer.remaining(); + if (remaining == 0 && buffer != QuicStreamReader.EOF) { + continue; // not yet EOF, so poll more + } + bytes += remaining; + processPeerControlBytes(buffer); + if (buffer == QuicStreamReader.EOF) { + // a EOF was processed, don't poll anymore + break; + } + } + if (debug.on()) { + debug.log("stop reading peer %s stream after %s bytes", + streamType, bytes); + } + } catch (IOException | RuntimeException | Error throwable) { + if (debug.on()) { + debug.log("Reading peer %s stream failed: %s", streamType, throwable); + } + // call the error handler and pass it the stream on which the error happened + errorHandler.onError(reader.stream(), this, throwable); + } + } + + /** + * Queues the given HTTP/3 stream type code on the given local unidirectional + * stream writer queue. + * @param stream a new local unidirectional stream + * @param code the code to queue up on the stream writer queue + * @return the given {@code stream} + */ + private QuicSenderStream openLocalStream(QuicSenderStream stream, int code) { + var streamType = localStreamType(); + if (debug.on()) { + debug.log("Opening local stream: %s %s(code=%s)", + stream.streamId(), streamType, code); + } + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = stream.connectWriter(scheduler); + try { + if (debug.on()) { + debug.log("Writing local stream type: stream %s %s(code=%s)", + stream.streamId(), streamType, code); + } + var buffer = ByteBuffer.allocate(VariableLengthEncoder.getEncodedSize(code)); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + writer.queueForWriting(buffer); + scheduler.stop(); + stream.disconnectWriter(writer); + } catch (Throwable t) { + if (debug.on()) { + debug.log("failed to create stream %s %s(code=%s): %s", + stream.streamId(), streamType, code, t); + } + try { + switch (streamType) { + case CONTROL, QPACK_ENCODER, QPACK_DECODER -> { + final String logMsg = "stream %s %s(code=%s)" + .formatted(stream.streamId(), streamType, code); + // TODO: revisit - we should probably invoke a method + // on the HttpQuicConnection or H3Connection instead of + // dealing directly with QuicConnection here. + final TerminationCause terminationCause = + appLayerClose(H3_STREAM_CREATION_ERROR.code()).loggedAs(logMsg); + quicConnection.connectionTerminator().terminate(terminationCause); + } + default -> writer.reset(H3_STREAM_CREATION_ERROR.code()); + } + } catch (Throwable suppressed) { + if (debug.on()) { + debug.log("couldn't close connection or reset stream: " + suppressed); + } + Utils.addSuppressed(t, suppressed); + throw new CompletionException(t); + } + } + return stream; + } + + public static interface StreamErrorHandler { + + /** + * Will be invoked when there is an error on a {@code QuicStream} handled by + * the {@code UniStreamPair} + * + * @param stream the stream on which the error occurred + * @param uniStreamPair the UniStreamPair to which the stream belongs + * @param error the error that occurred + */ + void onError(QuicStream stream, UniStreamPair uniStreamPair, Throwable error); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java new file mode 100644 index 00000000000..8487100c8ca --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPACK.StreamPairSupplier; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.DecoderInstructionsWriter; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.qpack.DynamicTable.ENTRY_SIZE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Decodes headers from their binary representation. + * + *

    Typical lifecycle looks like this: + * + *

    {@link #Decoder(StreamPairSupplier, QPACKErrorHandler) new Decoder} + * ({@link #configure(ConnectionSettings)} called once from our HTTP/3 settings + * {@link #decodeHeader(ByteBuffer, boolean, HeaderFrameReader) decodeHeader} + * + *

    {@code Decoder} does not require a complete header block in a single + * {@code ByteBuffer}. The header block can be spread across many buffers of any + * size and decoded one-by-one the way it makes most sense for the user. This + * way also allows not to limit the size of the header block. + * + *

    Headers are delivered to the {@linkplain DecodingCallback callback} as + * soon as they become decoded. Using the callback also gives the user freedom + * to decide how headers are processed. The callback does not limit the number + * of headers decoded during single decoding operation. + */ + +public final class Decoder { + + private final QPACK.Logger logger; + private final DynamicTable dynamicTable; + private final EncoderInstructionsReader encoderInstructionsReader; + private final QueuingStreamPair decoderStreamPair; + private static final AtomicLong DECODERS_IDS = new AtomicLong(); + // ID of last acknowledged entry acked by Insert Count Increment + // or section acknowledgement instruction + private long acknowledgedInsertsCount; + private final ReentrantLock ackInsertCountLock = new ReentrantLock(); + private final AtomicLong blockedStreamsCounter = new AtomicLong(); + private volatile long maxBlockedStreams; + private final QPACKErrorHandler qpackErrorHandler; + private volatile long maxFieldSectionSize = DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; + private final AtomicLong concurrentDynamicTableInsertions = + new AtomicLong(); + private static final long MAX_LITERAL_WITH_INDEXING = + Utils.getIntegerNetProperty("jdk.httpclient.maxLiteralWithIndexing", 512); + + /** + * Constructs a {@code Decoder} with zero initial capacity of the dynamic table. + * + *

    Dynamic table capacity values has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK. + *

    Maximum dynamic table capacity is determined by the value of SETTINGS_QPACK_MAX_TABLE_CAPACITY + * HTTP/3 setting sent by the decoder side (see + * + * 3.2.3. Maximum Dynamic Table Capacity). + *

    An encoder informs the decoder of a change to the dynamic table capacity using the + * "Set Dynamic Table Capacity" instruction + * (see + * 4.3.1. Set Dynamic Table Capacity) + * + * @see Decoder#configure(ConnectionSettings) + */ + public Decoder(StreamPairSupplier streams, QPACKErrorHandler errorHandler) { + long id = DECODERS_IDS.incrementAndGet(); + logger = QPACK.getLogger().subLogger("Decoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> "New decoder"); + } + dynamicTable = new DynamicTable(logger.subLogger("DynamicTable"), false); + decoderStreamPair = streams.create(this::processEncoderInstruction); + qpackErrorHandler = errorHandler; + encoderInstructionsReader = new EncoderInstructionsReader(new DecoderTableCallback(), logger); + } + + public QueuingStreamPair decoderStreams() { + return decoderStreamPair; + } + + /** + * {@return a new {@link HeaderFrameReader} that will hold the decoding + * state for a new request/response stream} + */ + public HeaderFrameReader newHeaderFrameReader(DecodingCallback decodingCallback) { + return new HeaderFrameReader(dynamicTable, decodingCallback, + blockedStreamsCounter, maxBlockedStreams, + maxFieldSectionSize, logger); + } + + public void ackTableInsertions() { + ackInsertCountLock.lock(); + try { + long insertCount = dynamicTable.insertCount(); + assert acknowledgedInsertsCount <= insertCount; + long incrementValue = insertCount - acknowledgedInsertsCount; + if (incrementValue > 0) { + // Write "Insert Count Increment" to the decoder stream + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instructionSize = decoderInstructionsWriter.configureForInsertCountInc(incrementValue); + submitDecoderInstruction(decoderInstructionsWriter, instructionSize); + } + // Update lastAck value + acknowledgedInsertsCount = insertCount; + } finally { + ackInsertCountLock.unlock(); + } + } + + /** + * Submit "Section Acknowledgment" instruction to the decoder stream. + * A field line section needs to be acknowledged after completion of + * section decoding. + * @param streamId stream ID associated with the field section's + * @param headerFrameReader header frame reader used to read + * the field line section + */ + public void ackSection(long streamId, HeaderFrameReader headerFrameReader) { + + FieldSectionPrefix prefix = headerFrameReader.decodedSectionPrefix(); + + // 4.4.1. Section Acknowledgment: If an encoder receives a Section Acknowledgment instruction + // referring to a stream on which every encoded field section with a non-zero Required Insert + // Count has already been acknowledged, this MUST be treated as a connection error of type + // QPACK_DECODER_STREAM_ERROR. + long prefixInsertCount = prefix.requiredInsertCount(); + if (prefixInsertCount == 0) return; + ackInsertCountLock.lock(); + try { + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instrSize = decoderInstructionsWriter.configureForSectionAck(streamId); + submitDecoderInstruction(decoderInstructionsWriter, instrSize); + if (prefixInsertCount > acknowledgedInsertsCount) { + acknowledgedInsertsCount = prefixInsertCount; + } + } finally { + ackInsertCountLock.unlock(); + } + } + + public void cancelStream(long streamId) { + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instrSize = decoderInstructionsWriter.configureForStreamCancel(streamId); + submitDecoderInstruction(decoderInstructionsWriter, instrSize); + dynamicTable.cleanupStreamInsertCountNotifications(streamId); + } + + /** + * Configures maximum capacity of the decoder's dynamic table based on connection settings of + * the HTTP client, also configures the number of allowed blocked streams. + * The decoder's dynamic table capacity can only be changed via + * {@linkplain EncoderInstructionsReader.Callback encoder instructions callback}. + * + * @param ourSettings connection settings + */ + public void configure(ConnectionSettings ourSettings) { + long maxCapacity = ourSettings.qpackMaxTableCapacity(); + dynamicTable.setMaxTableCapacity(maxCapacity); + maxBlockedStreams = ourSettings.qpackBlockedStreams(); + long maxFieldSS = ourSettings.maxFieldSectionSize(); + if (maxFieldSS > 0) { + maxFieldSectionSize = maxFieldSS; + } else { + // Unlimited field section size + maxFieldSectionSize = -1L; + } + } + + /** + * Decodes a header block from the given buffer to the given callback. + * + *

    Suppose a header block is represented by a sequence of + * {@code ByteBuffer}s in the form of {@code Iterator}. And the + * consumer of decoded headers is represented by {@linkplain DecodingCallback the callback} + * registered within the provided {@code headerFrameReader}. + * Then to decode the header block, the following approach might be used: + * {@snippet : + * HeaderFrameReader headerFrameReader = + * newHeaderFrameReader(decodingCallback); + * while (buffers.hasNext()) { + * ByteBuffer input = buffers.next(); + * decoder.decodeHeader(input, !buffers.hasNext(), headerFrameReader); + * } + * } + * + *

    The decoder reads as much as possible of the header block from the + * given buffer, starting at the buffer's position, and increments its + * position to reflect the bytes read. The buffer's mark and limit will not + * be modified. + * + *

    Once the method is invoked with {@code endOfHeaderBlock == true}, the + * current header block is deemed ended, and inconsistencies, if any, are + * reported immediately via a callback registered within the {@code + * headerFrameReader} instance. + * + *

    Each callback method is called only after the implementation has + * processed the corresponding bytes. If the bytes revealed a decoding + * error it is reported via a callback registered within the {@code + * headerFrameReader} instance. + * + * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of + * returning it for two reasons. The first one is that the user of the + * decoder always knows which chunk is the last. The second one is to throw + * the most detailed exception possible, which might be useful for + * diagnosing issues. + * + * @implNote This implementation is not atomic in respect to decoding + * errors. In other words, if the decoding operation has thrown a decoding + * error, the decoder is no longer usable. + * + * @param headerBlock + * the chunk of the header block, may be empty + * @param endOfHeaderBlock + * true if the chunk is the final (or the only one) in the sequence + * @param headerFrameReader the stateful header frame reader + * @throws NullPointerException + * if either {@code headerBlock} or {@code headerFrameReader} are null + */ + public void decodeHeader(ByteBuffer headerBlock, boolean endOfHeaderBlock, + HeaderFrameReader headerFrameReader) { + requireNonNull(headerFrameReader, "headerFrameReader"); + headerFrameReader.read(headerBlock, endOfHeaderBlock); + } + + /** + * This method is invoked when the {@linkplain + * UniStreamPair#futureReceiverStreamReader() decoder's stream reader} + * has data available for reading. + */ + private void processEncoderInstruction(ByteBuffer buffer) { + if (buffer == QuicStreamReader.EOF) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + qpackErrorHandler.closeOnError( + new ProtocolException("QPACK " + decoderStreamPair.remoteStreamType() + + " remote stream was unexpectedly closed"), H3_CLOSED_CRITICAL_STREAM); + return; + } + try { + int stringLengthLimit = Math.clamp(dynamicTable.capacity() - ENTRY_SIZE, + 0, Integer.MAX_VALUE - (int) ENTRY_SIZE); + encoderInstructionsReader.read(buffer, stringLengthLimit); + } catch (QPackException qPackException) { + qpackErrorHandler.closeOnError(qPackException.getCause(), qPackException.http3Error()); + } + } + + private void submitDecoderInstruction(DecoderInstructionsWriter decoderInstructionsWriter, + int size) { + if (size > decoderStreamPair.credit()) { + qpackErrorHandler.closeOnError( + new IOException("QPACK not enough credit on a decoder stream " + + decoderStreamPair.remoteStreamType()), H3_CLOSED_CRITICAL_STREAM); + return; + } + // All decoder instructions contain only one variable length integer. + // Which could take up to 9 bytes max. + ByteBuffer buffer = ByteBuffer.allocate(size); + boolean done = decoderInstructionsWriter.write(buffer); + // Assert that instruction is fully written, ie the correct + // instruction size estimation was supplied. + assert done; + buffer.flip(); + decoderStreamPair.submitData(buffer); + } + + void incrementAndCheckDynamicTableInsertsCount() { + if (MAX_LITERAL_WITH_INDEXING > 0) { + long concurrentNumberOfInserts = concurrentDynamicTableInsertions.incrementAndGet(); + if (concurrentNumberOfInserts > MAX_LITERAL_WITH_INDEXING) { + String exceptionMessage = "Too many literal with indexing: %s > %s" + .formatted(concurrentNumberOfInserts, MAX_LITERAL_WITH_INDEXING); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> exceptionMessage); + } + throw QPackException.encoderStreamError(new ProtocolException(exceptionMessage)); + } + } + } + + public void resetInsertionsCounter() { + if (MAX_LITERAL_WITH_INDEXING > 0) { + concurrentDynamicTableInsertions.set(0); + } + } + + private class DecoderTableCallback implements EncoderInstructionsReader.Callback { + + private void ensureInstructionsAllowed() { + // RFC9204 3.2.3. Maximum Dynamic Table Capacity: + // "When the maximum table capacity is zero, the encoder MUST NOT + // insert entries into the dynamic table and MUST NOT send any encoder + // instructions on the encoder stream." + if (dynamicTable.maxCapacity() == 0) { + throw new IllegalStateException("Unexpected encoder instruction"); + } + } + + @Override + public void onCapacityUpdate(long capacity) { + if (capacity == 0 && dynamicTable.maxCapacity() == 0) { + return; + } + ensureInstructionsAllowed(); + dynamicTable.setCapacity(capacity); + } + + @Override + public void onInsert(String name, String value) { + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (dynamicTable.insert(name, value) != DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough evictable space in dynamic table to insert entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + // RFC9204 7.4. Implementation Limits: + // "If an implementation encounters a value larger than it is able to decode, this MUST be + // treated as a stream error of type QPACK_DECOMPRESSION_FAILED if on a request stream or + // a connection error of the appropriate type if on the encoder or decoder stream." + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (dynamicTable.insert(nameIndex, indexInStaticTable, valueString) != + DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough space in dynamic table to insert entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + + @Override + public void onDuplicate(long l) { + // RFC9204 7.4. Implementation Limits: + // "If an implementation encounters a value larger than it is able to decode, this + // MUST be treated as a stream error of type QPACK_DECOMPRESSION_FAILED" + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, + () -> format("Processing duplicate instruction (%d)", l)); + } + if (dynamicTable.duplicate(l) != DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough space in dynamic table to duplicate entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java new file mode 100644 index 00000000000..bb9dbdadf59 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; + +import java.nio.ByteBuffer; + +/** + * Delivers results of the {@link Decoder#decodeHeader(ByteBuffer, boolean, HeaderFrameReader)} + * decoding operation. + * + *

    Methods of the callback are never called by a decoder with any of the + * arguments being {@code null}. + * + * @apiNote + * + *

    The callback provides methods for all possible + * + * field line representations. + * + *

    Names and values are {@link CharSequence}s rather than {@link String}s in + * order to allow users to decide whether they need to create objects. A + * {@code CharSequence} might be used in-place, for example, to be appended to + * an {@link Appendable} (e.g. {@link StringBuilder}) and then discarded. + * + *

    That said, if a passed {@code CharSequence} needs to outlast the method + * call, it needs to be copied. + * + */ +public interface DecodingCallback { + + /** + * A method the more specific methods of the callback forward their calls + * to. + * + * @param name + * header name + * @param value + * header value + */ + void onDecoded(CharSequence name, CharSequence value); + + /** + * A header fields decoding is completed. + */ + void onComplete(); + + /** + * A connection-level error observed during the decoding process. + * + * @param throwable a {@code Throwable} instance + * @param http3Error a HTTP3 error code + */ + void onConnectionError(Throwable throwable, Http3Error http3Error); + + /** + * A stream-level error observed during the decoding process. + * + * @param throwable a {@code Throwable} instance + * @param http3Error a HTTP3 error code + */ + default void onStreamError(Throwable throwable, Http3Error http3Error) { + onConnectionError(throwable, http3Error); + } + + /** + * Reports if {@linkplain #onConnectionError(Throwable, Http3Error) a connection} + * or {@linkplain #onStreamError(Throwable, Http3Error) a stream} error has been + * observed during the decoding process + * @return true - if error was observed; false - otherwise + */ + default boolean hasError() { + return false; + } + + /** + * Returns request/response stream id or push stream id associated with a decoding callback. + */ + long streamId(); + + /** + * A more finer-grained version of {@link #onDecoded(CharSequence, + * CharSequence)} that also reports on value sensitivity. + * + *

    Value sensitivity must be considered, for example, when implementing + * an intermediary. A {@code value} is sensitive if it was represented as Literal Header + * Field Never Indexed. + * + * @implSpec + * + *

    The default implementation invokes {@code onDecoded(name, value)}. + * + * @param name + * header name + * @param value + * header value + * @param sensitive + * whether the value is sensitive + */ + default void onDecoded(CharSequence name, + CharSequence value, + boolean sensitive) { + onDecoded(name, value); + } + + /** + * An Indexed + * Field Line decoded. + * + * @implSpec + * + *

    The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of a name/value pair in static or dynamic table + * @param name + * header name + * @param value + * header value + */ + default void onIndexed(long index, CharSequence name, CharSequence value) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Field Line with Name Reference decoded, where a {@code name} was + * referred by an {@code index}. + * + * @implSpec + * + *

    The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + * @param hideIntermediary + * if the header field should be written to intermediary nodes + */ + default void onLiteralWithNameReference(long index, + CharSequence name, + CharSequence value, + boolean valueHuffman, + boolean hideIntermediary) { + onDecoded(name, value, hideIntermediary); + } + + /** + * A Literal Field + * Line with Literal Name decoded, where both a {@code name} and a {@code value} + * were literal. + * + * @implSpec + * + *

    The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithLiteralName(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman, + boolean hideIntermediary) { + onDecoded(name, value, hideIntermediary); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java new file mode 100644 index 00000000000..6f5e567e182 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java @@ -0,0 +1,1069 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.Encoder.EncodingContext; +import jdk.internal.net.http.qpack.Encoder.SectionReference; +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; + +/* + * The dynamic table to store header fields. Implements dynamic table described + * in "QPACK: Header Compression for HTTP/3" RFC. + * The size of the table is the sum of the sizes of its entries. + */ +public final class DynamicTable implements HeadersTable { + + // QPACK Section 3.2.1: + // The size of an entry is the sum of its name's length in bytes, + // its value's length in bytes, and 32 additional bytes. + public static final long ENTRY_SIZE = 32L; + + // Initial length of the elements array + // It is required for this value to be a power of 2 integer + private static final int INITIAL_HOLDER_ARRAY_LENGTH = 64; + + final Logger logger; + + // Capacity (Maximum size) in bytes (or capacity in RFC 9204) of the dynamic table + private long capacity; + + // RFC-9204: 3.2.3. Maximum Dynamic Table Capacity + private long maxCapacity; + + // Max entries is required to implement encoding of Required Insert Count + // in Field Lines Prefix: + // RFC-9204: 4.5.1.1. Required Insert Count: + // "This encoding limits the length of the prefix on long-lived connections." + private long maxEntries; + + // Size of the dynamic table in bytes - calculated as the sum of the sizes of its entries. + private long size; + + // Table elements holder and its state variables + // Absolute ID of tail and head elements. + // tail id - is an id of the oldest element in the table + // head id - is an id of the next element that will be added to the table. + // head element id is head - 1. + // drain id - is the lowest element id that encoder can reference + private long tail, head, drain = -1; + + // Used space percentage threshold when to start increasing the drain index + private final int drainUsedSpaceThreshold = QPACK.ENCODER_DRAINING_THRESHOLD; + + // true - table is used by the QPack encoder, otherwise used by the + // QPack decoder + private final boolean encoderTable; + + // Array that holds dynamic table entries + private HeaderField[] elements; + + // name -> (value -> [index]) + private final Map>> indicesMap; + + private record TableInsertCountNotification(long streamId, long minimumRIC, + CompletableFuture completion) { + public boolean isStreamId(long streamId) { + return this.streamId == streamId; + } + public boolean isFulfilled(long insertionCount) { + return insertionCount >= minimumRIC; + } + } + + private final Queue insertCountNotifications = + new PriorityQueue<>( + Comparator.comparingLong(TableInsertCountNotification::minimumRIC) + ); + + public CompletableFuture awaitFutureInsertCount(long streamId, + long valueToAwait) { + if (encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var completion = new CompletableFuture(); + long insertCount = insertCount(); + if (insertCount >= valueToAwait) { + completion.complete(null); + } else { + insertCountNotifications + .add(new TableInsertCountNotification( + streamId, valueToAwait, completion)); + } + return completion; + } finally { + writeLock.unlock(); + } + } + + private void notifyInsertCountChange() { + assert lock.isWriteLockedByCurrentThread(); + if (insertCountNotifications.isEmpty()) { + return; + } + long insertCount = insertCount(); + Predicate isFulfilled = + icn -> icn.isFulfilled(insertCount); + insertCountNotifications.removeIf(icn -> completeIf(isFulfilled, icn)); + } + + public boolean cleanupStreamInsertCountNotifications(long streamId) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + Predicate isSameStreamId = + icn -> icn.isStreamId(streamId); + return insertCountNotifications.removeIf(icn -> completeIf(isSameStreamId, icn)); + } finally { + writeLock.unlock(); + } + } + + private static boolean completeIf(Predicate predicate, + TableInsertCountNotification insertCountNotification) { + if (predicate.test(insertCountNotification)) { + insertCountNotification.completion.complete(null); + return true; + } + return false; + } + + // Read-Write lock to manage access to table entries + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + public DynamicTable(Logger logger) { + this(logger, true); + } + + public DynamicTable(Logger logger, boolean encoderTable) { + this.logger = logger; + this.encoderTable = encoderTable; + elements = new HeaderField[INITIAL_HOLDER_ARRAY_LENGTH]; + indicesMap = new HashMap<>(); + // -1 signifies that max table capacity was not yet initialized + maxCapacity = -1L; + maxEntries = 0L; + } + + /** + * Returns size of the dynamic table in bytes + * @return size of the dynamic table + */ + public long size() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return size; + } finally { + readLock.unlock(); + } + } + + /** + * Returns current capacity of the dynamic table + * @return current capacity + */ + public long capacity() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return capacity; + } finally { + readLock.unlock(); + } + } + + /** + * Returns a maximum capacity in bytes of the dynamic table. + * @return maximum capacity + */ + public long maxCapacity() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return maxCapacity; + } finally { + readLock.unlock(); + } + } + + /** + * Sets a maximum capacity in bytes of the dynamic table. + * + *

    The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK + * (see + * 3.2.3 Maximum Dynamic Table Capacity). + * + *

    May be called only once to set maximum dynamic table capacity. + *

    This method doesn't change the actual capacity of the dynamic table. + * + * @see #setCapacity(long) + * @param maxCapacity a non-negative long + * @throws IllegalArgumentException if max capacity is negative + * @throws IllegalStateException if max capacity was already set + */ + public void setMaxTableCapacity(long maxCapacity) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (maxCapacity < 0) { + throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity); + } + if (this.maxCapacity != -1L) { + // Max table capacity is initialized from SETTINGS frame which can be only received once: + // "If an endpoint receives a second SETTINGS frame on the control stream, + // the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED" + // [RFC 9114 https://www.rfc-editor.org/rfc/rfc9114.html#name-settings] + throw new IllegalStateException("Max Table Capacity can only be set once"); + } + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("setting maximum allowed dynamic table capacity to %s", + maxCapacity)); + } + this.maxCapacity = maxCapacity; + this.maxEntries = maxCapacity / ENTRY_SIZE; + } finally { + writeLock.unlock(); + } + } + + /** + * Returns maximum possible number of entries that could be stored in the dynamic table + * with respect to MAX_CAPACITY setting. + * @return max entries + */ + public long maxEntries() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return maxEntries; + } finally { + readLock.unlock(); + } + } + + /** + * Retrieves a header field by its absolute index. Entry referenced by an absolute + * index does not depend on the state of the dynamic table. + * @param uniqueID an entry unique index + * @return retrieved header field + * @throws IllegalArgumentException if entry is not received yet, + * already evicted or invalid entry index is specified. + */ + @Override + public HeaderField get(long uniqueID) { + var readLock = lock.readLock(); + readLock.lock(); + try { + if (uniqueID < 0) { + throw new IllegalArgumentException("Entry index invalid"); + } + // Not yet received entry + if (uniqueID >= head) { + throw new IllegalArgumentException("Entry not received yet"); + } + // Already evicted entry + if (uniqueID < tail) { + throw new IllegalArgumentException("Entry already evicted"); + } + return elements[(int) (uniqueID & (elements.length - 1))]; + } finally { + readLock.unlock(); + } + } + + /** + * Retrieves a header field by its relative index. Entry referenced by a relative index depends + * on the state of the dynamic table. + * @param relativeId index relative to the most recently inserted entry + * @return retrieved header field + */ + public HeaderField getRelative(long relativeId) { + // RFC 9204: 3.2.5. Relative Indexing + // "Relative indices begin at zero and increase in the opposite direction from the absolute index. + // Determining which entry has a relative index of 0 depends on the context of the reference. + // In encoder instructions (Section 4.3), a relative index of 0 refers to the most recently inserted + // value in the dynamic table." + var readLock = lock.readLock(); + readLock.lock(); + try { + return get(insertCount() - 1 - relativeId); + } finally { + readLock.unlock(); + } + } + + /** + * Converts absolute entry index to relative index that can be used + * in the encoder instructions. + * Relative index of 0 refers to the most recently inserted entry. + * + * @param absoluteId absolute index of an entry + * @return relative entry index + */ + public long toRelative(long absoluteId) { + var readLock = lock.readLock(); + readLock.lock(); + try { + assert absoluteId < head; + return head - 1 - absoluteId; + } finally { + readLock.unlock(); + } + } + + /** + * Search an absolute id of a name:value pair in the dynamic table. + * @param name a name to search for + * @param value a value to search for + * @return positive index if name:value match found, + * negative index if only name match found, + * 0 if no match found + */ + @Override + public long search(String name, String value) { + // This method is only designated for encoder use + if (!encoderTable) { + return 0; + } + var readLock = lock.readLock(); + readLock.lock(); + try { + Map> values = indicesMap.get(name); + if (values == null) { + return 0; + } + Deque indexes = values.get(value); + if (indexes != null) { + // "+1" since the index range [0..id] is mapped to [1..id+1] + return indexes.peekLast() + 1; + } else { + assert !values.isEmpty(); + Long any = values.values().iterator().next().peekLast(); // Iterator allocation + // Use last entry in found values with matching name, and use its index for + // encoding with name reference. + // Negation and "-1" since name-only matches are mapped from [0..id] to + // [-1..-id-1] region + return -any - 1; + } + } finally { + readLock.unlock(); + } + } + + /** + * Add an entry to the dynamic table. + * Entries could be evicted from the dynamic table. + * Unacknowledged section references are not checked by this method, therefore + * this method is intended to be used by the decoder only. The encoder should use + * overloaded method that takes global unacknowledged section reference. + * + * @param name header name + * @param value header value + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + */ + @Override + public long insert(String name, String value) { + // Invoking toString() will possibly allocate Strings. But that's + // unavoidable at this stage. If a CharSequence is going to be stored in + // the table, it must not be mutable (e.g. for the sake of hashing). + return insert(new HeaderField(name, value), SectionReference.noReferences()); + } + + + /** + * Add entry to the dynamic table with name specified as index in static + * or dynamic table. + * Entries could be evicted from the dynamic table. + * Unacknowledged section references are not checked by this method, therefore + * this method is intended to be used by the decoder only. The encoder should use + * overloaded method that takes global unacknowledged section reference. + * + * @param nameIndex index of the header name to add + * @param isStaticIndex if name index references static table header name + * @param value header value + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + * @throws IllegalStateException if table memory reclamation error observed + */ + public long insert(long nameIndex, boolean isStaticIndex, String value) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Inserting with name index (nameIndex='%s' isStaticIndex='%s' value=%s)", + nameIndex, isStaticIndex, value)); + } + String name = isStaticIndex ? + StaticTable.HTTP3.get(nameIndex).name() : + getRelative(nameIndex).name(); + return insert(name, value); + } finally { + writeLock.unlock(); + } + } + + /** + * Add an entry to the dynamic table. + * Entries could be evicted from the dynamic table. + * The supplied unacknowledged section references are checked by this method to check + * if entries are evictable. + * Such checks are performed when there is not enough space in the dynamic table to insert + * the requested header. + * This method is intended to be used by the encoder only. + * + * @param name header name + * @param value header value + * @param sectionReference unacknowledged section references + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + * @throws IllegalStateException if table memory reclamation error observed + */ + public long insert(String name, String value, SectionReference sectionReference) { + return insert(new HeaderField(name, value), sectionReference); + } + + /** + * Inserts an entry to the dynamic table and sends encoder insert instruction bytes + * to the peer decoder. + * This method is designated to be used by the {@link Encoder} class only. + * If an entry with matching name:value is available, its index is returned + * and no insert instruction is generated on encoder stream. If duplicate entry is required + * due to entry being non-referencable then {@link DynamicTable#duplicateWithEncoderStreamUpdate( + * EncoderInstructionsWriter, long, QueuingStreamPair, EncodingContext)} is used. + * + * @param entry table entry to add + * @param writer non-configured encoder instruction writer for generating encoder + * instruction + * @param encoderStreams encoder stream pair + * @param encodingContext encoder encoding context + * @return absolute id of inserted entry OR already available entry, -1L if entry cannot + * be added + */ + public long insertWithEncoderStreamUpdate(TableEntry entry, + EncoderInstructionsWriter writer, + QueuingStreamPair encoderStreams, + EncodingContext encodingContext) { + if (!encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + String name = entry.name().toString(); + String value = entry.value().toString(); + // Entry with name only match in dynamic table + boolean nameOnlyDynamicEntry = !entry.isStaticTable() && entry.type() == NAME; + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + // First, check if entry is in the table already - + // no need to add a new one. + long index = search(name, value); + if (index > 0) { + long absIndex = index - 1; + // Check if found entry can be referenced, + // if not issue duplicate instruction + if (!canReferenceEntry(absIndex)) { + return duplicateWithEncoderStreamUpdate(writer, + absIndex, encoderStreams, encodingContext); + } + return absIndex; + } + SectionReference evictionLimitSR = encodingContext.evictionLimit(); + if (nameOnlyDynamicEntry) { + long nameIndex = entry.index(); + if (!canReferenceEntry(nameIndex)) { + return ENTRY_NOT_INSERTED; + } + evictionLimitSR = evictionLimitSR.reduce(nameIndex); + encodingContext.registerSessionReference(nameIndex); + } + // Relative index calculation should precede the insertion + // due to dependency on insert count value + long relativeNameIndex = + nameOnlyDynamicEntry ? toRelative(entry.index()) : -1; + + // Insert new entry to the table with respect to entry + // references range provided by the encoding context + long idx = insert(name, value, evictionLimitSR); + if (idx == ENTRY_NOT_INSERTED) { + // Insertion requires eviction of entries from unacknowledged + // sections therefore entry is not added + return ENTRY_NOT_INSERTED; + } + // Entry was successfully inserted + if (nameOnlyDynamicEntry) { + // Absolute index only needs to be replaced with the relative one + // when it references a name in the dynamic table. + entry = entry.relativizeDynamicTableEntry(relativeNameIndex); + } + int instructionSize = writer.configureForEntryInsertion(entry); + writeEncoderInstruction(writer, instructionSize, encoderStreams); + return idx; + } finally { + writeLock.unlock(); + } + } + + private long insert(HeaderField h, SectionReference sectionReference) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("adding ('%s', '%s')", h.name(), h.value())); + } + long entrySize = headerSize(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s", h.name(), h.value(), entrySize)); + } + + long availableEvictableSpace = availableEvictableSpace(sectionReference); + if (availableEvictableSpace < entrySize) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Header size exceeds available evictable space=%s." + + " Combined section reference=%s", + availableEvictableSpace, sectionReference)); + } + // Evicting entries won't help to gather enough space to insert the requested one + return ENTRY_NOT_INSERTED; + } + while (entrySize > capacity - size && size != 0) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("insufficient space %s, must evict entry", (capacity - size))); + } + // Only Encoder will supply section with referenced + // entries + if (sectionReference.referencesEntries()) { + // Check if tail element is evictable + if (tail < sectionReference.min()) { + if (!evictEntry()) { + return ENTRY_NOT_INSERTED; + } + } else { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Cannot evict entry: sectionRef=%s tail=%s", + sectionReference, tail)); + } + // For now -1 is returned to notify the Encoder that entry + // cannot be inserted to the dynamic table + return ENTRY_NOT_INSERTED; + } + } else { + // This call can be called by both Encoder and Decoder: + // - Encoder when add new entry with no unacked section references + // - Decoder when processing insert entry instructions. + // Entries are evicted until there is enough space OR until table + // is empty. + if (!evictEntry()) { + return ENTRY_NOT_INSERTED; + } + } + } + size += entrySize; + // At this stage it is clear that there are enough bytes (max capacity is not exceeded) in the dynamic + // table to add new header field + addWithInverseMapping(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("('%s, '%s') added", h.name(), h.value())); + logger.log(EXTRA, this::toString); + } + notifyInsertCountChange(); + return head - 1; + } finally { + writeLock.unlock(); + } + } + + public long duplicate(long relativeId) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var entry = getRelative(relativeId); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Duplicate entry with absId=%s" + + " insertCount=%s ('%s', '%s')", + insertCount() - 1 - relativeId, insertCount(), + entry.name(), entry.value())); + } + return insert(entry.name(), entry.value()); + } finally { + writeLock.unlock(); + } + } + + public long duplicateWithEncoderStreamUpdate(EncoderInstructionsWriter writer, + long absoluteEntryId, + QueuingStreamPair encoderStreams, + EncodingContext encodingContext) { + if (!encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var entry = get(absoluteEntryId); + // Relative index calculation should precede the insertion + // due to dependency on insert count value + long relativeEntryId = toRelative(absoluteEntryId); + + // Make entry id that needs to be duplicated non-evictable + SectionReference evictionLimit = encodingContext.evictionLimit() + .reduce(absoluteEntryId); + + // Put duplicated entry to our dynamic table first + long idx = insert(entry.name(), entry.value(), + evictionLimit); + if (idx == ENTRY_NOT_INSERTED) { + return ENTRY_NOT_INSERTED; + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Issuing entry duplication instruction" + + " for absId=%s relId=%s ('%s', '%s')", + absoluteEntryId, relativeEntryId, entry.name(), entry.value())); + } + + // Configure writer for entry duplication + int instructionSize = + writer.configureForEntryDuplication(relativeEntryId); + + // Write instruction to the encoder stream + writeEncoderInstruction(writer, instructionSize, encoderStreams); + return idx; + } finally { + writeLock.unlock(); + } + } + + private HeaderField remove() { + assert lock.isWriteLockedByCurrentThread(); + // Remove element from the holder array first + if (getElementsCount() == 0) { + throw new IllegalStateException("Empty table"); + } + + int tailIdx = (int) (tail++ & (elements.length - 1)); + HeaderField f = elements[tailIdx]; + elements[tailIdx] = null; + + // Log the removal event + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("removing ('%s', '%s')", f.name(), f.value())); + } + + // Update indices map on the encoder table only + if (encoderTable) { + Map> values = indicesMap.get(f.name()); + Deque indexes = values.get(f.value()); + // Remove the oldest index of the name:value pair + Long index = indexes.pollFirst(); + // Clean-up indexes associated with a value from values map + if (indexes.isEmpty()) { + values.remove(f.value()); + } + assert index != null; + // If indexes map associated with name is empty remove name + // entry from indices map + if (values.isEmpty()) { + indicesMap.remove(f.name()); + } + } + return f; + } + + /** + * Sets the dynamic table capacity in bytes. + * The new capacity must be lower than or equal to the limit defined by + * SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 settings parameter. This limit is + * enforced by {@linkplain DynamicTable#setMaxTableCapacity(long)}. + * + * @param capacity dynamic table capacity to set + */ + public void setCapacity(long capacity) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (capacity > maxCapacity) { + // Calling code catches IllegalArgumentException and generates the connection error: + // 4.3.1. Set Dynamic Table Capacity: + // "The decoder MUST treat a new dynamic table capacity value that exceeds + // this limit as a connection error of type QPACK_ENCODER_STREAM_ERROR." + throw new IllegalArgumentException("Illegal dynamic table capacity"); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: capacity=" + capacity); + } + while (capacity < size && size != 0) { + // Evict entries until existing elements fit into + // new table capacity + boolean entryEvicted = evictEntry(); + assert entryEvicted; + } + this.capacity = capacity; + if (usedSpace() < drainUsedSpaceThreshold) { + if (drain != -1) { + drain = -1; + } + } else if (drain == -1 || tail > drain) { + drain = tail; + } + } finally { + writeLock.unlock(); + } + } + + /** + * Updates the capacity of the dynamic table and sends encoder capacity update instruction + * bytes to the peer decoder. + * This method is designated to be used by the {@link Encoder} class only. + * @param writer non-configured encoder instruction writer for generating encoder instruction + * @param capacity new capacity value + * @param encoderStreams encoder stream pair + */ + public void setCapacityWithEncoderStreamUpdate(EncoderInstructionsWriter writer, long capacity, + QueuingStreamPair encoderStreams) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + // Configure writer for capacity update + int instructionSize = writer.configureForTableCapacityUpdate(capacity); + // Check and set our capacity + setCapacity(capacity); + // Write instruction + writeEncoderInstruction(writer, instructionSize, encoderStreams); + } finally { + writeLock.unlock(); + } + } + + + /** + * Evicts one entry from the table tail. + * @return {@code true} if entry was evicted, + * {@code false} if nothing to remove + */ + private boolean evictEntry() { + assert lock.isWriteLockedByCurrentThread(); + try { + HeaderField f = remove(); + long s = headerSize(f); + this.size -= s; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> format("evicted entry ('%s', '%s') of size %s with absId=%s", + f.name(), f.value(), s, tail - 1)); + } + } catch (IllegalStateException ise) { + // Entry cannot be evicted from empty table + return false; + } + return true; + } + + public long availableEvictableSpace(SectionReference sectionReference) { + var readLock = lock.readLock(); + readLock.lock(); + try { + if (!sectionReference.referencesEntries()) { + return capacity; + } + // Size that can be reclaimed in the dynamic table by evicting + // non-referenced entries + long availableEvictableCapacity = 0; + for (long absId = tail; absId < sectionReference.min(); absId++) { + HeaderField field = get(absId); + availableEvictableCapacity += headerSize(field); + } + // (capacity - size) - free space in the dynamic table + return availableEvictableCapacity + (capacity - size); + } finally { + readLock.unlock(); + } + } + + public boolean tryReferenceEntry(TableEntry tableEntry, EncodingContext context) { + var readLock = lock.readLock(); + readLock.lock(); + try { + long absId = tableEntry.index(); + if (canReferenceEntry(absId)) { + context.registerSessionReference(absId); + context.referenceEntry(tableEntry); + return true; + } else { + return false; + } + } finally { + readLock.unlock(); + } + } + + @Override + public String toString() { + var readLock = lock.readLock(); + readLock.lock(); + try { + double used = usedSpace(); + return format("full length: %s, used space: %s/%s (%.1f%%)", + getElementsCount(), size, capacity, used); + } finally { + readLock.unlock(); + } + } + + private boolean canReferenceEntry(long absId) { + // The dynamic table lock is acquired by the calling methods + return absId > drain; + } + + private double usedSpace() { + return capacity == 0 ? 0 : 100 * (((double) size) / capacity); + } + + public static long headerSize(HeaderField f) { + return headerSize(f.name(), f.value()); + } + + public static long headerSize(String name, String value) { + return name.length() + value.length() + ENTRY_SIZE; + } + + // To quickly find an index of an entry in the dynamic table with the + // given contents an effective inverse mapping is needed. + private void addWithInverseMapping(HeaderField field) { + assert lock.isWriteLockedByCurrentThread(); + // Check if holder array has at least one free slot to add header field + // The method below can increase elements.length if no free slot found + ensureElementsArrayLength(); + long counterSnapshot = head++; + elements[(int) (counterSnapshot & (elements.length - 1))] = field; + if (encoderTable) { + // Allocate unique index and use it to store in indicesMap + Map> values = indicesMap.computeIfAbsent( + field.name(), _ -> new HashMap<>()); + Deque indexes = values.computeIfAbsent( + field.value(), _ -> new LinkedList<>()); + indexes.add(counterSnapshot); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> format("added '%s' header field with '%s' unique id", + field, counterSnapshot)); + } + assert indexesUniqueAndOrdered(indexes); + // Draining index is only used by the Encoder + updateDrainIndex(); + } + } + + private void updateDrainIndex() { + if (!encoderTable) { + return; + } + assert lock.isWriteLockedByCurrentThread(); + if (usedSpace() > drainUsedSpaceThreshold) { + if (drain == -1L) { + drain = tail; + } else { + drain++; + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Draining index changed: %d", drain)); + } + } + } + + private void ensureElementsArrayLength() { + assert lock.isWriteLockedByCurrentThread(); + + int currentArrayLength = elements.length; + if (getElementsCount() == currentArrayLength) { + if (currentArrayLength == (1 << 30)) { + throw new IllegalStateException("No room for more elements"); + } + // Increase elements array by factor of 2 + resize(currentArrayLength << 1); + } + } + + private boolean indexesUniqueAndOrdered(Deque indexes) { + long maxIndexSoFar = -1L; + for (long l : indexes) { + if (l <= maxIndexSoFar) { + return false; + } else { + maxIndexSoFar = l; + } + } + return true; + } + + private int getElementsCount() { + // head and tail are unique and monotonic indexes - therefore we can just use their + // difference to determine number of header:value pairs stored in the dynamic table. + // Since head points to the next unused element head == 0 means that there is + // no elements in the dynamic table + return head > 0 ? (int) (head - tail) : 0; + } + + private void resize(int newSize) { + // newSize is always a power of 2: + // - its initial size is a power of 2 + // - it is shifted 1 bit left every + // time when there is not enough space in the 'elements' array + assert lock.isWriteLockedByCurrentThread(); + int elementsCnt = getElementsCount(); + final int oldSize = elements.length; + + if (newSize < elementsCnt) { + throw new IllegalArgumentException("New size is too low to hold existing elements"); + } + + HeaderField[] newElements = new HeaderField[newSize]; + if (elementsCnt == 0) { + elements = newElements; + return; + } + long headID = head - 1; + final int oldTailIdx = (int) (tail & (oldSize - 1)); + final int oldHeadIdx = (int) (headID & (oldSize - 1)); + final int newTailIdx = (int) (tail & (newSize - 1)); + final int newHeadIdx = (int) (headID & (newSize - 1)); + + if (oldTailIdx <= oldHeadIdx) { + // Elements in an old array are stored in a continuous segment + if (newTailIdx <= newHeadIdx) { + // Elements in a new array will be stored in a continuous segment + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, elementsCnt); + } else { + // Elements in a new array will split in two segments due to wrapping around + // the end of a new array. + int sizeFromNewTailToEnd = newSize - newTailIdx; + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, sizeFromNewTailToEnd); + System.arraycopy(elements, oldTailIdx + sizeFromNewTailToEnd, + newElements, 0, newHeadIdx + 1); + } + } else { + // Elements in an old array are split in two segments + if (newTailIdx <= newHeadIdx) { + // Elements in a new array will be stored in a continuous segment + int firstSegmentSize = oldSize - oldTailIdx; + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, firstSegmentSize); + System.arraycopy(elements, 0, + newElements, newTailIdx + firstSegmentSize, oldHeadIdx + 1); + } else { + // Elements in a new array will be stored in two segments + // Size from the tail to the end in an old array + int oldPart1Size = oldSize - oldTailIdx; + // Size from the tail to the end in a new array + int newPart1Size = newSize - newTailIdx; + if (oldPart1Size <= newPart1Size) { + // Segment from tail to the end of an old array + // fits into the corresponding segment in a new array + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, oldPart1Size); + int leftToCopyToNewPart1 = newPart1Size - oldPart1Size; + System.arraycopy(elements, 0, newElements, + newTailIdx + oldPart1Size, leftToCopyToNewPart1); + System.arraycopy(elements, leftToCopyToNewPart1, + newElements, 0, newHeadIdx + 1); + } else { // oldPart1Size > newPart1Size + // Not possible given two restrictions: + // - we do not allow rewriting of entries if size is not enough, + // IAE is thrown above. + // - the size of elements holder array can only be a power of 2 + throw new AssertionError("Not possible dynamic table indexes configuration"); + } + } + } + elements = newElements; + } + + /** + * Method returns number of elements inserted to the dynamic table. + * Since element ids start from 0 the returned value is equal + * to the id of the head element plus one. + * @return number of elements in the dynamic table + */ + public long insertCount() { + var rl = lock.readLock(); + rl.lock(); + try { + // head points to the next unallocated element + return head; + } finally { + rl.unlock(); + } + } + + // Writes an encoder instruction to the encoder stream associated with dynamic table. + // This method is kept in DynamicTable class since most instructions depend on + // and/or update the dynamic table state. + // Also, we want to send encoder instruction and update the dynamic table state while + // holding the write-lock. + private void writeEncoderInstruction(EncoderInstructionsWriter writer, int instructionSize, + QueuingStreamPair encoderStreams) { + if (instructionSize > encoderStreams.credit()) { + throw new QPackException(H3_CLOSED_CRITICAL_STREAM, + new IOException("QPACK not enough credit on an encoder stream " + + encoderStreams.remoteStreamType()), true); + } + boolean done; + ByteBuffer buffer; + do { + if (instructionSize > MAX_BUFFER_SIZE) { + buffer = ByteBuffer.allocate(MAX_BUFFER_SIZE); + instructionSize -= MAX_BUFFER_SIZE; + } else { + buffer = ByteBuffer.allocate(instructionSize); + } + done = writer.write(buffer); + buffer.flip(); + encoderStreams.submitData(buffer); + } while (!done); + } + private static final int MAX_BUFFER_SIZE = 1024 * 16; + static final long ENTRY_NOT_INSERTED = -1L; +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java new file mode 100644 index 00000000000..26da27a702a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java @@ -0,0 +1,672 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPACK.StreamPairSupplier; +import jdk.internal.net.http.qpack.TableEntry.EntryType; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.internal.net.http.qpack.writers.FieldLineSectionPrefixWriter; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Encodes headers to their binary representation. + */ +public class Encoder { + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + // RFC 9204 7.1.3. Never-Indexed Literals: + // "Implementations can also choose to protect sensitive fields by not + // compressing them and instead encoding their value as literals" + private static final Set SENSITIVE_HEADER_NAMES = + Set.of("cookie", "authorization", "proxy-authorization"); + + private final Logger logger; + private final InsertionPolicy policy; + private final TablesIndexer tablesIndexer; + private final DynamicTable dynamicTable; + private final QueuingStreamPair encoderStreams; + private final DecoderInstructionsReader decoderInstructionsReader; + // RFC-9204: 2.1.4. Known Received Count + private long knownReceivedCount; + + // Lock for Known Received Count variable + private final ReentrantReadWriteLock krcLock = new ReentrantReadWriteLock(); + + // Max blocked streams setting value received from the peer decoder + // can be set only once + private long maxBlockedStreams = -1L; + + // Number of streams in process of headers encoding that expected to be blocked + // but their unacknowledged section is not registered yet + private long blockedStreamsInFlight; + + private final ReentrantLock blockedStreamsCounterLock = new ReentrantLock(); + + // stream id -> fifo list of max and min ids referenced from field sections for each stream id + private final ConcurrentMap> unacknowledgedSections = + new ConcurrentHashMap<>(); + + // stream id -> set of referenced entry absolute indexes from a field line section that currently + // are in process of encoding and not added to the unacknowledged field sections map yet. + private final ConcurrentMap> liveContextReferences = + new ConcurrentHashMap<>(); + + private final QPACKErrorHandler qpackErrorHandler; + + public HeaderFrameWriter newHeaderFrameWriter() { + return new HeaderFrameWriter(logger); + } + + /** + * Constructs an {@code Encoder} with zero initial capacity of the dynamic table. + * Maximum dynamic table capacity is not initialized until peer (decoder) HTTP/3 settings frame is + * received (see {@link Encoder#configure(ConnectionSettings)}). + * + *

    Dynamic table capacity values has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK. + *

    Maximum dynamic table capacity is determined by the value of SETTINGS_QPACK_MAX_TABLE_CAPACITY + * HTTP/3 setting sent by the decoder side (see + * + * 3.2.3. Maximum Dynamic Table Capacity). + *

    An encoder informs the decoder of a change to the dynamic table capacity using the + * "Set Dynamic Table Capacity" instruction + * (see + * 4.3.1. Set Dynamic Table Capacity) + * + * @param streamPairs supplier of the encoder unidirectional stream pair + * @throws IllegalArgumentException if maxCapacity is negative + * @see Encoder#configure(ConnectionSettings) + */ + public Encoder(InsertionPolicy policy, StreamPairSupplier streamPairs, QPACKErrorHandler codingError) { + this.policy = policy; + long id = ENCODERS_IDS.incrementAndGet(); + this.logger = QPACK.getLogger().subLogger("Encoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> "New encoder"); + } + if (logger.isLoggable(EXTRA)) { + /* To correlate with logging outside QPACK, knowing + hashCode/toString is important */ + logger.log(EXTRA, () -> { + String hashCode = Integer.toHexString( + System.identityHashCode(this)); + /* Since Encoder can be subclassed hashCode AND identity + hashCode might be different. So let's print both. */ + return format("toString='%s', hashCode=%s, identityHashCode=%s", + this, hashCode(), hashCode); + }); + } + // Set maximum dynamic table to 0, postpone setting of max capacity until peer + // settings frame is received + dynamicTable = new DynamicTable(logger.subLogger("DynamicTable"), true); + tablesIndexer = new TablesIndexer(StaticTable.HTTP3, dynamicTable); + encoderStreams = streamPairs.create(this::processDecoderAcks); + decoderInstructionsReader = new DecoderInstructionsReader(new TableUpdatesCallback(), + logger); + qpackErrorHandler = codingError; + } + + /** + * Configures encoder according to the settings received from the peer. + * + * @param peerSettings the peer settings + */ + public void configure(ConnectionSettings peerSettings) { + blockedStreamsCounterLock.lock(); + try { + if (maxBlockedStreams == -1) { + maxBlockedStreams = peerSettings.qpackBlockedStreams(); + } else { + throw new IllegalStateException("Encoder already configured"); + } + } finally { + blockedStreamsCounterLock.unlock(); + } + // Set max dynamic table capacity + long maxCapacity = peerSettings.qpackMaxTableCapacity(); + dynamicTable.setMaxTableCapacity(maxCapacity); + // Send DT capacity update instruction if the peer negotiated non-zero + // max table capacity, and limit the value with encoder's table capacity + // limit system property value + if (QPACK.ENCODER_TABLE_CAPACITY_LIMIT > 0 && maxCapacity > 0) { + long encoderCapacity = Math.min(maxCapacity, QPACK.ENCODER_TABLE_CAPACITY_LIMIT); + setTableCapacity(encoderCapacity); + } + } + + public QueuingStreamPair encoderStreams() { + return encoderStreams; + } + + public void header(EncodingContext context, CharSequence name, CharSequence value, + boolean sensitive) throws IllegalStateException { + header(context, name, value, sensitive, knownReceivedCount()); + } + + /** + * Sets up the given header {@code (name, value)} with possibly sensitive + * value. + * + *

    If the {@code value} is sensitive (think security, secrecy, etc.) + * this encoder will compress it using a special representation + * (see + * 7.1.3. Never-Indexed Literals). + * + *

    Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param context the encoding context + * @param name the name + * @param value the value + * @param sensitive whether the value is sensitive + * @param knownReceivedCount the count of received entries known to a peer decoder or + * {@code -1} to skip the dynamic table entry index check during header encoding. + * @throws NullPointerException if any of the arguments are {@code null} + * @throws IllegalStateException if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) + */ + public void header(EncodingContext context, CharSequence name, CharSequence value, + boolean sensitive, long knownReceivedCount) throws IllegalStateException { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s", + name, value, sensitive)); + } + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + // TablesIndexer.entryOf checks if the found entry is a dynamic table entry, + // and if its insertion was already ACKed. If not - use literal or name index encoding. + var tableEntry = tablesIndexer.entryOf(name, value, knownReceivedCount); + + // NAME_VALUE table entry type means that one of dynamic or static tables contain + // exact name:value pair. + if (dynamicTable.capacity() > 0L + && tableEntry.type() != EntryType.NAME_VALUE + && !sensitive && policy.shouldUpdateDynamicTable(tableEntry)) { + // We should check if we have an entry in dynamic table: + // - If we have it - do nothing + // - if we do not have it - insert it and use the index straight-away + // when blocking encoding is allowed + tableEntry = context.tryInsertEntry(tableEntry); + } + + // First, check that found/newly inserted entry is in the dynamic table + // and can be referenced + if (!tableEntry.isStaticTable() && tableEntry.index() >= 0 && + tableEntry.type() != EntryType.NEITHER) { + if (!dynamicTable.tryReferenceEntry(tableEntry, context)) { + // If entry cannot be referenced - use literal encoding instead + tableEntry = tableEntry.toLiteralsEntry(); + } + } + + // Configure header frame writer to write header field to the headers frame. One of the following + // writers is selected based on entry type, the base value and the referenced table (static or dynamic): + // - static table and name:value match - "Indexed Field Line" + // - static table and name match - "Literal Field Line with Name Reference" + // - dynamic table, name:value match and index < base - "Indexed Field Line" + // - dynamic table, name match and index < base - "Literal Field Line with Name Reference" + // - dynamic table, name:value match and index >= base - "Indexed Field Line with Post-Base Index" + // - dynamic table, name match and index >= base - "Literal Field Line with Post-Base Name Reference" + // - not in dynamic or static tables - "Literal Field Line with Literal Name" + context.writer.configure(tableEntry, sensitive, context.base); + } + + /** + * Sets the capacity of the encoder's dynamic table and notifies the decoder by + * issuing "Set Dynamic Table Capacity" instruction. + * + *

    The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK + * (see + * 4.3.1. Set Dynamic Table Capacity). + * + * @param capacity a non-negative long + * @throws IllegalArgumentException if capacity is negative or exceeds the negotiated max capacity HTTP/3 setting + */ + public void setTableCapacity(long capacity) { + dynamicTable.setCapacityWithEncoderStreamUpdate(new EncoderInstructionsWriter(logger), + capacity, encoderStreams); + } + + /** + * This method is called when the peer decoder sends + * data on the peer's decoder stream + * + * @param buffer data sent by the peer's decoder + */ + private void processDecoderAcks(ByteBuffer buffer) { + if (buffer == QuicStreamReader.EOF) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + qpackErrorHandler.closeOnError( + new ProtocolException("QPACK " + encoderStreams.remoteStreamType() + + " remote stream was unexpectedly closed"), H3_CLOSED_CRITICAL_STREAM); + return; + } + try { + decoderInstructionsReader.read(buffer); + } catch (QPackException e) { + qpackErrorHandler.closeOnError(e.getCause(), e.http3Error()); + } + } + + public List encodeHeaders(HeaderFrameWriter writer, long streamId, + int bufferSize, HttpHeaders... headers) { + List buffers = new ArrayList<>(); + ByteBuffer buffer = getByteBuffer(bufferSize); + + try (EncodingContext encodingContext = newEncodingContext(streamId, + dynamicTable.insertCount(), writer)) { + for (HttpHeaders header : headers) { + for (Map.Entry> e : header.map().entrySet()) { + // RFC-9114, section 4.2: Field names are strings containing a subset of + // ASCII characters. .... Characters in field names MUST be converted to + // lowercase prior to their encoding. + final String lKey = e.getKey().toLowerCase(Locale.ROOT); + final List values = e.getValue(); + // An encoder might also choose not to index values for fields that are + // considered to be highly valuable or sensitive to recovery, such as the + // Cookie or Authorization header fields + final boolean sensitive = SENSITIVE_HEADER_NAMES.contains(lKey); + for (String value : values) { + header(encodingContext, lKey, value, sensitive); + while (!writer.write(buffer)) { + buffer.flip(); + buffers.add(buffer); + buffer = getByteBuffer(bufferSize); + } + } + } + } + buffer.flip(); + buffers.add(buffer); + + // Put field line section prefix as the first byte buffer + generateFieldLineSectionPrefix(encodingContext, buffers); + + // Register field line section as unacked if it uses references to the + // dynamic table entries + registerUnackedFieldLineSection(streamId, SectionReference.of(encodingContext)); + } + return buffers; + } + + public void generateFieldLineSectionPrefix(EncodingContext encodingContext, List buffers) { + // Write field section prefix according to RFC 9204: "4.5.1. Encoded Field Section Prefix" + FieldLineSectionPrefixWriter prefixWriter = new FieldLineSectionPrefixWriter(); + FieldSectionPrefix fsp = encodingContext.sectionPrefix(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Encoding Field Section Prefix - required insert" + + " count: %d base: %d", + fsp.requiredInsertCount(), fsp.base())); + } + int requiredSize = prefixWriter.configure(fsp, dynamicTable.maxEntries()); + var fspBuffer = getByteBuffer(requiredSize); + if (!prefixWriter.write(fspBuffer)) { + throw new IllegalStateException("Field Line Section Prefix"); + } + fspBuffer.flip(); + buffers.addFirst(fspBuffer); + } + + public void registerUnackedFieldLineSection(long streamId, SectionReference sectionReference) { + if (sectionReference.referencesEntries()) { + unacknowledgedSections + .computeIfAbsent(streamId, k -> new ConcurrentLinkedQueue<>()) + .add(sectionReference); + } + } + + // This one is for tracking evict-ability of dynamic table entries + public SectionReference unackedFieldLineSectionsRange(EncodingContext context) { + SectionReference referenceNotRegisteredYet = SectionReference.of(context); + return unackedFieldLineSectionsRange(referenceNotRegisteredYet); + } + + private SectionReference unackedFieldLineSectionsRange(SectionReference initial) { + return unacknowledgedSections.values().stream() + .flatMap(Queue::stream) + .reduce(initial, SectionReference::reduce); + } + + long blockedStreamsCount() { + long blockedStreams = 0; + long krc = knownReceivedCount(); + for (var streamSections : unacknowledgedSections.values()) { + boolean hasBlockedSection = streamSections.stream() + .anyMatch(sectionReference -> !sectionReference.fullyAcked(krc)); + blockedStreams = hasBlockedSection ? blockedStreams + 1 : blockedStreams; + } + return blockedStreams; + } + + + public long knownReceivedCount() { + krcLock.readLock().lock(); + try { + return knownReceivedCount; + } finally { + krcLock.readLock().unlock(); + } + } + + private void updateKrcSectionAck(long streamId) { + krcLock.writeLock().lock(); + try { + var queue = unacknowledgedSections.get(streamId); + // max() + 1 - since it is "Required Insert Count" not entry ID + SectionReference oldestSectionRef = queue != null ? queue.poll() : null; + long oldestNonAckedRic = oldestSectionRef != null ? oldestSectionRef.max() + 1 : -1L; + if (oldestNonAckedRic == -1L) { + // RFC 9204 4.4.1. Section Acknowledgment: + // If an encoder receives a Section Acknowledgment instruction referring + // to a stream on which every encoded field section with a non-zero + // Required Insert Count has already been acknowledged, this MUST be treated + // as a connection error of type QPACK_DECODER_STREAM_ERROR. + var qPackException = QPackException.decoderStreamError( + new IllegalStateException("No unacknowledged sections found" + + " for stream id = " + streamId)); + throw qPackException; + } + // "2.1.4. Known Received Count": + // If the Required Insert Count of the acknowledged field section is greater + // than the current Known Received Count, the Known Received Count is updated + // to that Required Insert Count value. + if (oldestNonAckedRic != -1 && knownReceivedCount < oldestNonAckedRic) { + knownReceivedCount = oldestNonAckedRic; + } + } finally { + krcLock.writeLock().unlock(); + } + } + + private void updateKrcInsertCountIncrement(long increment) { + long insertCount = dynamicTable.insertCount(); + krcLock.writeLock().lock(); + try { + // An encoder that receives an Increment field equal to zero, or one that increases + // the Known Received Count beyond what the encoder has sent, MUST treat this as + // a connection error of type QPACK_DECODER_STREAM_ERROR. + if (increment == 0 || knownReceivedCount > insertCount - increment) { + var qpackException = QPackException.decoderStreamError( + new IllegalStateException("Invalid increment field value: " + increment)); + throw qpackException; + } + knownReceivedCount += increment; + } finally { + krcLock.writeLock().unlock(); + } + } + + private void cleanupStreamData(long streamId) { + liveContextReferences.remove(streamId); + unacknowledgedSections.remove(streamId); + } + + private class TableUpdatesCallback implements DecoderInstructionsReader.Callback { + @Override + public void onSectionAck(long streamId) { + updateKrcSectionAck(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + updateKrcInsertCountIncrement(increment); + } + + @Override + public void onStreamCancel(long streamId) { + cleanupStreamData(streamId); + } + } + + public class EncodingContext implements AutoCloseable { + final long base; + final long streamId; + final ConcurrentSkipListSet referencedIndexes; + long maxIndex; + long minIndex; + boolean blockedDecoderExpected; + final HeaderFrameWriter writer; + final EncoderInstructionsWriter encoderInstructionsWriter; + + public EncodingContext(long streamId, long base, HeaderFrameWriter writer) { + this.base = base; + this.encoderInstructionsWriter = new EncoderInstructionsWriter(logger); + this.writer = writer; + this.maxIndex = -1L; + this.minIndex = Long.MAX_VALUE; + this.streamId = streamId; + this.referencedIndexes = liveContextReferences.computeIfAbsent(streamId, + _ -> new ConcurrentSkipListSet<>()); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Begin encoding session with base = %s stream-id = %s", base, streamId)); + } + } + + public void registerSessionReference(long absoluteEntryId) { + referencedIndexes.add(absoluteEntryId); + } + + @Override + public void close() { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Closing encoding context for stream-id=%s" + + " session references:%s", + streamId, referencedIndexes)); + } + liveContextReferences.remove(streamId); + // Deregister if this stream was marked as in-flight blocked + blockedStreamsCounterLock.lock(); + try { + if (blockedDecoderExpected) { + blockedStreamsInFlight--; + } + } finally { + blockedStreamsCounterLock.unlock(); + } + } + + public FieldSectionPrefix sectionPrefix() { + // RFC 9204: 2.1.2. Blocked Streams + // "the Required Insert Count is one larger than the largest absolute index + // of all referenced dynamic table entries" + // largestAbsoluteIndex is initialized to -1, and if there is no dynamic + // table entry references - RIC will be set to 0. + return new FieldSectionPrefix(maxIndex + 1, base); + } + + public SectionReference evictionLimit() { + // In-flight references - a set with entry ids referenced from all + // active header encoding sessions not fully encoded yet + SectionReference inFlightReferences = SectionReference.singleReference( + liveContextReferences.values().stream() + .filter(Predicate.not(ConcurrentSkipListSet::isEmpty)) + .map(ConcurrentSkipListSet::first) + .min(Long::compare) + .orElse(-1L)); + + // Calculate the eviction limit with respect to: + // - in-flight references + // - acknowledged dynamic table insertions + // - range of unacknowledged sections which already fully encoded + // and sent as part of other request/response streams + return inFlightReferences + .reduce(knownReceivedCount()) + .reduce(unackedFieldLineSectionsRange(this)); + } + + public TableEntry tryInsertEntry(TableEntry entry) { + long idx = dynamicTable.insertWithEncoderStreamUpdate(entry, + encoderInstructionsWriter, encoderStreams, + this); + if (idx == DynamicTable.ENTRY_NOT_INSERTED) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Not adding entry '%s' to the dynamic " + + "table - not enough space, or unacknowledged entry needs to be evicted", + entry)); + } + // Return what we previously found in the dynamic or static table + return entry; + } + + if (QPACK.ALLOW_BLOCKING_ENCODING && canReferenceNewEntry()) { + // Create a new TableEntry that describes newly added header field + return entry.toNewDynamicTableEntry(idx); + } else { + return entry; + } + } + + private boolean canReferenceNewEntry() { + blockedStreamsCounterLock.lock(); + try { + // If current encoding context is already marked as blocked we can + // reference new entries without analyzing number of blocked streams + if (blockedDecoderExpected) { + return true; + } + // Number of streams with unacknowledged field line section + long alreadyBlocked = blockedStreamsCount(); + // Other streams might be in progress of headers encoding + boolean canReferenceNewEntry = maxBlockedStreams - alreadyBlocked - blockedStreamsInFlight > 0; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("%s reference to newly added header. " + + "Number of blocked streams based on unAcked sections: %d " + + "Number of blocked streams in progress of encoding: %d " + + "Max allowed by HTTP/3 settings: %d", + canReferenceNewEntry ? "Allowing" : "Restricting", + alreadyBlocked, blockedStreamsInFlight, maxBlockedStreams)); + } + if (canReferenceNewEntry && !blockedDecoderExpected) { + blockedStreamsInFlight++; + blockedDecoderExpected = true; + } + return canReferenceNewEntry; + } finally { + blockedStreamsCounterLock.unlock(); + } + } + + public void referenceEntry(TableEntry tableEntry) { + assert tableEntry.index() >= 0; + if (!tableEntry.isStaticTable()) { + long index = tableEntry.index(); + maxIndex = Long.max(maxIndex, index); + minIndex = Long.min(minIndex, index); + } + } + } + + /** + * Descriptor of entries range referenced from a field lines section. + * + * @param min minimum entry id referenced from a field lines section + * @param max maximum entry id referenced from a field lines section + */ + public record SectionReference(long min, long max) { + public static SectionReference of(EncodingContext context) { + if (context.maxIndex == -1L) { + return SectionReference.noReferences(); + } + return new SectionReference(context.minIndex, context.maxIndex); + } + + public SectionReference reduce(SectionReference other) { + if (!referencesEntries()) { + return other; + } else if (!other.referencesEntries()) { + return this; + } + long newMin = Long.min(this.min, other.min); + long newMax = Long.max(this.max, other.max); + return new SectionReference(newMin, newMax); + } + + public SectionReference reduce(long entryId) { + return reduce(singleReference(entryId)); + } + + public static SectionReference singleReference(long entryId) { + return new SectionReference(entryId, entryId); + } + + public boolean fullyAcked(long knownReceiveCount) { + return max < knownReceiveCount; + } + + public static SectionReference noReferences() { + return new SectionReference(-1L, -1L); + } + + public boolean referencesEntries() { + return max != -1L; + } + } + + public EncodingContext newEncodingContext(long streamId, long base, HeaderFrameWriter writer) { + assert streamId >= 0; + assert base >= 0; + return new EncodingContext(streamId, base, writer); + } + + private ByteBuffer getByteBuffer(int size) { + ByteBuffer buf = ByteBuffer.allocate(size); + buf.limit(size); + return buf; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java new file mode 100644 index 00000000000..cbf2037af6a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +public record FieldSectionPrefix(long requiredInsertCount, long base) { + + public static FieldSectionPrefix decode(long encodedRIC, long deltaBase, + int baseSign, DynamicTable dynamicTable) { + long decodedRIC = decodeRIC(encodedRIC, dynamicTable); + long decodedBase = decodeBase(decodedRIC, deltaBase, baseSign); + return new FieldSectionPrefix(decodedRIC, decodedBase); + } + + private static long decodeRIC(long encodedRIC, DynamicTable dynamicTable) { + if (encodedRIC == 0) { + return 0; + } + long maxEntries = dynamicTable.maxEntries(); + long insertCount = dynamicTable.insertCount(); + long fullRange = 2 * maxEntries; + if (encodedRIC > fullRange) { + throw decompressionFailed(); + } + long maxValue = insertCount + maxEntries; + long maxWrapped = (maxValue/fullRange) * fullRange; + long ric = maxWrapped + encodedRIC - 1; + if (ric > maxValue) { + if (ric <= fullRange) { + throw decompressionFailed(); + } + ric -= fullRange; + } + + if (ric == 0) { + throw decompressionFailed(); + } + return ric; + } + + private static long decodeBase(long decodedRic, long deltaBase, int signBit) { + if (signBit == 0) { + return decodedRic + deltaBase; + } else { + return decodedRic - deltaBase - 1; + } + } + + private static QPackException decompressionFailed() { + var decompressionFailed = new IllegalStateException("QPACK decompression failed"); + return QPackException.decompressionFailed(decompressionFailed, true); + } +} diff --git a/src/java.base/macosx/classes/sun/nio/fs/MacOSXNativeDispatcher.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java similarity index 74% rename from src/java.base/macosx/classes/sun/nio/fs/MacOSXNativeDispatcher.java rename to src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java index a90db9d1f4e..83ee21eda30 100644 --- a/src/java.base/macosx/classes/sun/nio/fs/MacOSXNativeDispatcher.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,17 +22,16 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ +package jdk.internal.net.http.qpack; -package sun.nio.fs; +public record HeaderField(String name, String value) { -/** - * MacOSX specific system calls. - */ + public HeaderField(String name) { + this(name, ""); + } -class MacOSXNativeDispatcher extends BsdNativeDispatcher { - private MacOSXNativeDispatcher() { } - - static final int kCFStringNormalizationFormC = 2; - static final int kCFStringNormalizationFormD = 0; - static native char[] normalizepath(char[] path, int form); + @Override + public String toString() { + return value.isEmpty() ? name : name + ":" + value; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java new file mode 100644 index 00000000000..4b70777401d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +public sealed interface HeadersTable permits StaticTable, DynamicTable { + + /** + * Add an entry to the table. + * + * @param name header name + * @param value header value + * @return unique index of entry added to the table. + * If element cannot be added {@code -1} is returned. + */ + long insert(String name, String value); + + /** + * Get a table entry with specified unique index. + * + * @param index an entry unique index + * @return table entry + */ + HeaderField get(long index); + + /** + * Returns an index for name:value pair, or just name in a headers table. + * The contract for return values is the following: + * - a positive integer {@code i} where {@code i - 1} is an index of an + * entry with a header (n, v), where {@code n.equals(name) && v.equals(value)}. + *

    + * - a negative integer {@code j} where {@code -j - 1} is an index of an entry with + * a header (n, v), where {@code n.equals(name)}. + *

    + * - {@code 0} if there's no entry 'e' found such that {@code e.getName().equals(name)} + * + * @param name a name to search for + * @param value a value to search for + * @return a non-zero value if a matching entry is found, 0 otherwise + */ + long search(String name, String value); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java new file mode 100644 index 00000000000..1bdf84304ca --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +public interface InsertionPolicy { + boolean shouldUpdateDynamicTable(TableEntry tableEntry); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java new file mode 100644 index 00000000000..44282e5ad53 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.QPACK.Logger.Level; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.lang.String.format; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ALLOW_BLOCKING_ENCODING; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_BLOCKED_STREAMS; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_MAX_TABLE_CAPACITY; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ENCODER_DRAINING_THRESHOLD; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ENCODER_TABLE_CAPACITY_LIMIT; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NONE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Internal utilities and stuff. + */ +public final class QPACK { + + + // A dynamic table capacity that the encoder is allowed to set given that it doesn't + // exceed the max capacity value negotiated by the decoder. If the max capacity + // less than this limit the encoder's dynamic table capacity is set to the max capacity + // value. + public static final long ENCODER_TABLE_CAPACITY_LIMIT = QPACK_ENCODER_TABLE_CAPACITY_LIMIT; + + // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long DECODER_MAX_TABLE_CAPACITY = QPACK_DECODER_MAX_TABLE_CAPACITY; + + // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long DECODER_MAX_FIELD_SECTION_SIZE = QPACK_DECODER_MAX_FIELD_SECTION_SIZE; + + // Decoder upper bound on the number of streams that can be blocked + public static final long DECODER_BLOCKED_STREAMS = QPACK_DECODER_BLOCKED_STREAMS; + + // If set to "true" allows the encoder to insert a header with a dynamic + // name reference and reference it in a field line section without awaiting + // decoder's acknowledgement. + public static final boolean ALLOW_BLOCKING_ENCODING = QPACK_ALLOW_BLOCKING_ENCODING; + + // Threshold of available dynamic table space after which the draining + // index starts increasing. This index determines which entries are + // too close to eviction, and can be referenced by the encoder + public static final int ENCODER_DRAINING_THRESHOLD = QPACK_ENCODER_DRAINING_THRESHOLD; + + private static final RootLogger LOGGER; + private static final Map logLevels = + Map.of("NORMAL", NORMAL, "EXTRA", EXTRA); + + static { + String PROPERTY = "jdk.internal.httpclient.qpack.log.level"; + String value = Utils.getProperty(PROPERTY); + + if (value == null) { + LOGGER = new RootLogger(NONE); + } else { + String upperCasedValue = value.toUpperCase(); + Level l = logLevels.get(upperCasedValue); + if (l == null) { + LOGGER = new RootLogger(NONE); + LOGGER.log(System.Logger.Level.INFO, + () -> format("%s value '%s' not recognized (use %s); logging disabled", + PROPERTY, value, String.join(", ", logLevels.keySet()))); + } else { + LOGGER = new RootLogger(l); + LOGGER.log(System.Logger.Level.DEBUG, + () -> format("logging level %s", l)); + } + } + } + + public static Logger getLogger() { + return LOGGER; + } + + public static SettingsFrame updateDecoderSettings(SettingsFrame defaultSettingsFrame) { + SettingsFrame settingsFrame = defaultSettingsFrame; + settingsFrame.setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, DECODER_BLOCKED_STREAMS); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DECODER_MAX_TABLE_CAPACITY); + settingsFrame.setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, DECODER_MAX_FIELD_SECTION_SIZE); + return settingsFrame; + } + + private QPACK() { } + + /** + * The purpose of this logger is to provide means of diagnosing issues _in + * the QPACK implementation_. It's not a general purpose logger. + */ + // implements System.Logger to make it possible to skip this class + // when looking for the Caller. + public static class Logger implements System.Logger { + + /** + * Log detail level. + */ + public enum Level { + + NONE(0, System.Logger.Level.OFF), + NORMAL(1, System.Logger.Level.DEBUG), + EXTRA(2, System.Logger.Level.TRACE); + + private final int level; + final System.Logger.Level systemLevel; + + Level(int i, System.Logger.Level system) { + level = i; + systemLevel = system; + } + + public final boolean implies(Level other) { + return this.level >= other.level; + } + } + + private final String name; + private final Level level; + private final String path; + private final System.Logger logger; + + private Logger(String path, String name, Level level) { + this.path = path; + this.name = name; + this.level = level; + this.logger = Utils.getHpackLogger(path::toString, level.systemLevel); + } + + public final String getName() { + return name; + } + + @Override + public boolean isLoggable(System.Logger.Level level) { + return logger.isLoggable(level); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown) { + logger.log(level, bundle, msg,thrown); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String format, Object... params) { + logger.log(level, bundle, format, params); + } + + /* + * Usual performance trick for logging, reducing performance overhead in + * the case where logging with the specified level is a NOP. + */ + + public boolean isLoggable(Level level) { + return this.level.implies(level); + } + + public void log(Level level, Supplier s) { + if (this.level.implies(level)) { + logger.log(level.systemLevel, s); + } + } + + public Logger subLogger(String name) { + return new Logger(path + "/" + name, name, level); + } + + } + + private static final class RootLogger extends Logger { + + protected RootLogger(Level level) { + super("qpack", "qpack", level); + } + + } + + // -- low-level utilities -- + + /** + * An interface used to obtain the encoder or decoder stream pair + * from the enclosing HTTP/3 connection. + */ + @FunctionalInterface + public interface StreamPairSupplier { + QueuingStreamPair create(Consumer receiver); + } + + public interface QPACKErrorHandler { + void closeOnError(Throwable throwable, Http3Error error); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java new file mode 100644 index 00000000000..6252a1b9549 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.Http3Error; + +/** + * Represents a QPack related failure as a failure cause and + * an HTTP/3 error code. + */ +public final class QPackException extends RuntimeException { + + @java.io.Serial + private static final long serialVersionUID = 8443631555257118370L; + + private final boolean isConnectionError; + private final Http3Error http3Error; + + public QPackException(Http3Error http3Error, Throwable cause, boolean isConnectionError) { + super(cause); + this.isConnectionError = isConnectionError; + this.http3Error = http3Error; + } + + public static QPackException encoderStreamError(Throwable cause) { + throw new QPackException(Http3Error.QPACK_ENCODER_STREAM_ERROR, cause, true); + } + + public static QPackException decoderStreamError(Throwable cause) { + throw new QPackException(Http3Error.QPACK_DECODER_STREAM_ERROR, cause, true); + } + + public static QPackException decompressionFailed(Throwable cause, boolean isConnectionError) { + throw new QPackException(Http3Error.QPACK_DECOMPRESSION_FAILED, cause, isConnectionError); + } + + + public Http3Error http3Error() { + return http3Error; + } + + public boolean isConnectionError() { + return isConnectionError; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java new file mode 100644 index 00000000000..52900a2d1c3 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/* + * A header table with most common header fields. + * This table was generated by analyzing actual Internet traffic in 2018 + * and is a part of "QPACK: Header Compression for HTTP/3" RFC. + */ +public final class StaticTable implements HeadersTable { + + /* An immutable list of static header fields */ + public static final List HTTP3_HEADER_FIELDS = List.of( + new HeaderField(":authority"), + new HeaderField(":path", "/"), + new HeaderField("age", "0"), + new HeaderField("content-disposition"), + new HeaderField("content-length", "0"), + new HeaderField("cookie"), + new HeaderField("date"), + new HeaderField("etag"), + new HeaderField("if-modified-since"), + new HeaderField("if-none-match"), + new HeaderField("last-modified"), + new HeaderField("link"), + new HeaderField("location"), + new HeaderField("referer"), + new HeaderField("set-cookie"), + new HeaderField(":method", "CONNECT"), + new HeaderField(":method", "DELETE"), + new HeaderField(":method", "GET"), + new HeaderField(":method", "HEAD"), + new HeaderField(":method", "OPTIONS"), + new HeaderField(":method", "POST"), + new HeaderField(":method", "PUT"), + new HeaderField(":scheme", "http"), + new HeaderField(":scheme", "https"), + new HeaderField(":status", "103"), + new HeaderField(":status", "200"), + new HeaderField(":status", "304"), + new HeaderField(":status", "404"), + new HeaderField(":status", "503"), + new HeaderField("accept", "*/*"), + new HeaderField("accept", "application/dns-message"), + new HeaderField("accept-encoding", "gzip, deflate, br"), + new HeaderField("accept-ranges", "bytes"), + new HeaderField("access-control-allow-headers", "cache-control"), + new HeaderField("access-control-allow-headers", "content-type"), + new HeaderField("access-control-allow-origin", "*"), + new HeaderField("cache-control", "max-age=0"), + new HeaderField("cache-control", "max-age=2592000"), + new HeaderField("cache-control", "max-age=604800"), + new HeaderField("cache-control", "no-cache"), + new HeaderField("cache-control", "no-store"), + new HeaderField("cache-control", "public, max-age=31536000"), + new HeaderField("content-encoding", "br"), + new HeaderField("content-encoding", "gzip"), + new HeaderField("content-type", "application/dns-message"), + new HeaderField("content-type", "application/javascript"), + new HeaderField("content-type", "application/json"), + new HeaderField("content-type", "application/x-www-form-urlencoded"), + new HeaderField("content-type", "image/gif"), + new HeaderField("content-type", "image/jpeg"), + new HeaderField("content-type", "image/png"), + new HeaderField("content-type", "text/css"), + new HeaderField("content-type", "text/html; charset=utf-8"), + new HeaderField("content-type", "text/plain"), + new HeaderField("content-type", "text/plain;charset=utf-8"), + new HeaderField("range", "bytes=0-"), + new HeaderField("strict-transport-security", "max-age=31536000"), + new HeaderField("strict-transport-security", "max-age=31536000; includesubdomains"), + new HeaderField("strict-transport-security", "max-age=31536000; includesubdomains; preload"), + new HeaderField("vary", "accept-encoding"), + new HeaderField("vary", "origin"), + new HeaderField("x-content-type-options", "nosniff"), + new HeaderField("x-xss-protection", "1; mode=block"), + new HeaderField(":status", "100"), + new HeaderField(":status", "204"), + new HeaderField(":status", "206"), + new HeaderField(":status", "302"), + new HeaderField(":status", "400"), + new HeaderField(":status", "403"), + new HeaderField(":status", "421"), + new HeaderField(":status", "425"), + new HeaderField(":status", "500"), + new HeaderField("accept-language"), + new HeaderField("access-control-allow-credentials", "FALSE"), + new HeaderField("access-control-allow-credentials", "TRUE"), + new HeaderField("access-control-allow-headers", "*"), + new HeaderField("access-control-allow-methods", "get"), + new HeaderField("access-control-allow-methods", "get, post, options"), + new HeaderField("access-control-allow-methods", "options"), + new HeaderField("access-control-expose-headers", "content-length"), + new HeaderField("access-control-request-headers", "content-type"), + new HeaderField("access-control-request-method", "get"), + new HeaderField("access-control-request-method", "post"), + new HeaderField("alt-svc", "clear"), + new HeaderField("authorization"), + new HeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"), + new HeaderField("early-data", "1"), + new HeaderField("expect-ct"), + new HeaderField("forwarded"), + new HeaderField("if-range"), + new HeaderField("origin"), + new HeaderField("purpose", "prefetch"), + new HeaderField("server"), + new HeaderField("timing-allow-origin", "*"), + new HeaderField("upgrade-insecure-requests", "1"), + new HeaderField("user-agent"), + new HeaderField("x-forwarded-for"), + new HeaderField("x-frame-options", "deny"), + new HeaderField("x-frame-options", "sameorigin") + ); + + public static final StaticTable HTTP3 = new StaticTable(HTTP3_HEADER_FIELDS); + + private final List headerFields; + private final Map> indicesMap; + + private StaticTable(List headerFields) { + this.headerFields = headerFields; + this.indicesMap = buildIndicesMap(headerFields); + } + + @Override + public HeaderField get(long index) { + if (index >= headerFields.size()) { + throw new IllegalArgumentException("Invalid static table entry index"); + } + return headerFields.get((int)index); + } + + @Override + public long insert(String name, String value) { + throw new UnsupportedOperationException("Operation not supported by static tables"); + } + + @Override + public long search(String name, String value) { + Map values = indicesMap.get(name); + // 0 return value if no match is found in the static table + int searchResult = 0; + if (values != null) { + Integer idx = values.get(value); + if (idx != null) { + searchResult = idx + 1; + } else { + // Only name is found - return first id from indices for the name provided + searchResult = -values.values().iterator().next() - 1; + } + } + return searchResult; + } + + private static Map> buildIndicesMap(List fields) { + int numEntries = fields.size(); + Map> map = new HashMap<>(numEntries); + for (int i = 0; i < numEntries; i++) { + HeaderField f = fields.get(i); + Map values = map.computeIfAbsent(f.name(), _ -> new HashMap<>()); + values.put(f.value(), i); + } + return map; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java new file mode 100644 index 00000000000..6603c1d4c44 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.hpack.QuickHuffman; + +// Record containing table information for entry +public record TableEntry(boolean isStaticTable, long index, CharSequence name, CharSequence value, + EntryType type, boolean huffmanName, boolean huffmanValue) { + + public TableEntry(boolean isStaticTable, long index, CharSequence name, CharSequence value, EntryType type) { + this(isStaticTable, index, name, value, type, + isHuffmanBetterFor(name, true, type), + isHuffmanBetterFor(value, false, type)); + } + + public TableEntry toNewDynamicTableEntry(long index) { + return new TableEntry(false, index, name, value, EntryType.NAME_VALUE); + } + + public TableEntry relativizeDynamicTableEntry(long relativeIndex) { + assert !isStaticTable; + assert relativeIndex >= 0; + return new TableEntry(false, relativeIndex, name, value, type); + } + + public TableEntry(CharSequence name, CharSequence value) { + this(false, -1L, name, value, EntryType.NEITHER, + isHuffmanBetterFor(name, true, EntryType.NEITHER), + isHuffmanBetterFor(value, false, EntryType.NEITHER)); + } + + public TableEntry toLiteralsEntry() { + return new TableEntry(name, value); + } + + /** + * EntryType describes the type of TableEntry as either: + *

    + * - NAME_VALUE: a table entry where both name and value exist in table + * - NAME: a table entry where only name is present in table + * - NEITHER: a table entry where neither name nor value have been found + */ + public enum EntryType {NAME_VALUE, NAME, NEITHER} + + static boolean isHuffmanBetterFor(CharSequence str, boolean isName, EntryType type) { + return switch (type) { + case NEITHER -> QuickHuffman.isHuffmanBetterFor(str); + case NAME_VALUE -> false; + case NAME -> !isName && QuickHuffman.isHuffmanBetterFor(str); + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java new file mode 100644 index 00000000000..5afe25d9781 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME_VALUE; + +/* + * Adds reverse lookup to dynamic and static tables. + * Decoder does not need this functionality. On the other hand, + * Encoder does. + */ +public final class TablesIndexer { + + private final DynamicTable dynamicTable; + private final StaticTable staticTable; + + public TablesIndexer(StaticTable staticTable, DynamicTable dynamicTable) { + this.dynamicTable = dynamicTable; + this.staticTable = staticTable; + } + + /** + * Searches in dynamic and static tables for an entry that has matching name + * or name:value. + * Found dynamic table entry ids are matched against provided + * known receive count value if it is non-negative. + * If known receive count value is negative the entry id check is + * not performed. + * + * @param name entry name to search + * @param value entry value to search + * @param knownReceivedCount known received count to match dynamic table + * entries, if negative - id check is not performed. + * @return a table entry that matches provided parameters + */ + public TableEntry entryOf(CharSequence name, CharSequence value, + long knownReceivedCount) { + // Invoking toString() will possibly allocate Strings for the sake of + // the searchDynamic, which doesn't feel right. + String n = name.toString(); + String v = value.toString(); + + // Tests can use -1 known receive count value to filter dynamic table + // entry ids. + boolean limitDynamicTableEntryIds = knownReceivedCount >= 0; + + // 1. Try exact match in the static table + var staticSearchResult = staticTable.search(n, v); + if (staticSearchResult > 0) { + // name:value pair is found in static table + return new TableEntry(true, staticSearchResult - 1, + name, value, NAME_VALUE); + } + // 2. Try exact match in the dynamic table + var dynamicSearchResult = dynamicTable.search(n, v); + if (dynamicSearchResult == 0 && staticSearchResult == 0) { + // dynamic and static tables do not contain name or name:value entries + // - use literal table entry + return new TableEntry(name, value); + } + long dtEntryId; + // name:value hit in dynamic table + if (dynamicSearchResult > 0) { + dtEntryId = dynamicSearchResult - 1; + if (!limitDynamicTableEntryIds || dtEntryId < knownReceivedCount) { + return new TableEntry(false, dtEntryId, name, value, + NAME_VALUE); + } + } + // Name only hit in the static table + if (staticSearchResult < 0) { + return new TableEntry(true, -staticSearchResult - 1, name, + value, NAME); + } + + // Name only hit in the dynamic table + if (dynamicSearchResult < 0) { + dtEntryId = -dynamicSearchResult - 1; + if (!limitDynamicTableEntryIds || dtEntryId < knownReceivedCount) { + return new TableEntry(false, dtEntryId, name, value, NAME); + } + } + + // No match found in the tables, or there is a dynamic table entry that has + // name or 'name:value' match but its index is greater than max allowed dynamic + // table index, ie the entry is not acknowledged by the decoder. + return new TableEntry(name, value); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java new file mode 100644 index 00000000000..d171e81dfbf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +/** + * QPACK (Header Compression for HTTP/3) implementation conforming to + * RFC 9204. + * + *

    Headers can be decoded and encoded by {@link jdk.internal.net.http.qpack.Decoder} + * and {@link jdk.internal.net.http.qpack.Encoder} respectively. + * + *

    Instances of these classes are not safe for use by multiple threads. + */ +package jdk.internal.net.http.qpack; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java new file mode 100644 index 00000000000..70ddace72ce --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.QPackException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECODER_STREAM_ERROR; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +/* + * Reader for decoder instructions described in RFC9204 + * "4.4 Encoder Instructions" section. + * Read instructions are passed to the consumer via the DecoderInstructionsReader.Callback + * instance supplied to the reader constructor. + */ +public class DecoderInstructionsReader { + enum State { + INIT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | Stream ID (7+) | + +---+---------------------------+ + */ + SECTION_ACKNOWLEDGMENT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | Stream ID (6+) | + +---+---+-----------------------+ + */ + STREAM_CANCELLATION, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | Increment (6+) | + +---+---+-----------------------+ + */ + INSERT_COUNT_INCREMENT + } + + private State state; + private final IntegerReader integerReader; + private final Callback callback; + private final Logger logger; + + public DecoderInstructionsReader(Callback callback, Logger logger) { + this.integerReader = new IntegerReader( + new ReaderError(QPACK_DECODER_STREAM_ERROR, true)); + this.callback = callback; + this.state = State.INIT; + this.logger = logger.subLogger("DecoderInstructionsReader"); + } + + public void read(ByteBuffer buffer) { + requireNonNull(buffer, "buffer"); + while (buffer.hasRemaining()) { + switch (state) { + case INIT: + integerReader.reset(); + state = identifyDecoderInstruction(buffer); + break; + case INSERT_COUNT_INCREMENT, SECTION_ACKNOWLEDGMENT, STREAM_CANCELLATION: + // All decoder instructions consists of only one variable + // length integer field, therefore we fully read integer and + // then call the callback method depending on the state value + if (integerReader.read(buffer)) { + long value = integerReader.get(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Instruction: %s value: %s", + state.name(), value)); + } + // dispatch instruction to the consumer via the callback + dispatchParsedInstruction(value); + state = State.INIT; + } + break; + } + } + } + + private State identifyDecoderInstruction(ByteBuffer buffer) { + int b = buffer.get(buffer.position()) & 0xFF; // absolute read + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + case 0 -> { + integerReader.configure(7); + yield State.SECTION_ACKNOWLEDGMENT; + } + case 1 -> { + integerReader.configure(6); + yield State.STREAM_CANCELLATION; + } + default -> { + if ((b & 0b1100_0000) == 0) { + integerReader.configure(6); + yield State.INSERT_COUNT_INCREMENT; + } else { + throw QPackException.decoderStreamError( + new IOException("Unexpected decoder instruction: " + b)); + } + } + }; + } + + private void dispatchParsedInstruction(long value) { + switch (state) { + case INSERT_COUNT_INCREMENT: + callback.onInsertCountIncrement(value); + break; + case SECTION_ACKNOWLEDGMENT: + callback.onSectionAck(value); + break; + case STREAM_CANCELLATION: + callback.onStreamCancel(value); + break; + default: + throw QPackException.decoderStreamError( + new IOException("Unknown decoder instruction")); + } + } + + public interface Callback { + void onSectionAck(long streamId); + + void onStreamCancel(long streamId); + + void onInsertCountIncrement(long increment); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java new file mode 100644 index 00000000000..ff8b2868639 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.lang.System.Logger.Level.TRACE; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.QPACK_ENCODER_STREAM_ERROR; + +/* + * Reader for encoder instructions defined in RFC9204 + * "4.3 Encoder Instructions" section. + * Read instruction is passed to the consumer via Callback + * interface supplied to the EncoderInstructionsReader constructor. + */ +public class EncoderInstructionsReader { + + enum State { + INIT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | Capacity (5+) | + +---+---+---+-------------------+ + */ + DT_CAPACITY, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Name Index (6+) | + +---+---+-----------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + INSERT_NAME_REF_NAME, + INSERT_NAME_REF_VALUE, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | H | Name Length (5+) | + +---+---+---+-------------------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + INSERT_NAME_LIT_NAME, + INSERT_NAME_LIT_VALUE, + + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | Index (5+) | + +---+---+---+-------------------+ + */ + DUPLICATE + } + + private final QPACK.Logger logger; + private final Callback updateCallback; + private State state; + private final IntegerReader integerReader; + private final StringReader stringReader; + private int bitT = -1; + private long nameIndex = -1L; + private boolean huffmanValue; + private final StringBuilder valueString = new StringBuilder(); + + private boolean huffmanName; + private final StringBuilder nameString = new StringBuilder(); + + public EncoderInstructionsReader(Callback dtUpdateCallback, QPACK.Logger logger) { + this.logger = logger; + this.updateCallback = dtUpdateCallback; + this.state = State.INIT; + var errorToReport = new ReaderError(QPACK_ENCODER_STREAM_ERROR, true); + this.integerReader = new IntegerReader(errorToReport); + this.stringReader = new StringReader(errorToReport); + } + + public void read(ByteBuffer buffer, int maxStringLength) { + try { + read0(buffer, maxStringLength); + } catch (IllegalArgumentException | IllegalStateException exception) { + // "Duplicate" and "Insert With Name Reference" instructions can reference + // non-existing entries in the dynamic table. + // Such errors are treated as encoder stream errors. + throw QPackException.encoderStreamError(exception); + } + } + + private void read0(ByteBuffer buffer, int maxStringLength) { + requireNonNull(buffer, "buffer"); + while (buffer.hasRemaining()) { + switch (state) { + case INIT: + state = identifyEncoderInstruction(buffer); + break; + case DT_CAPACITY: + if (integerReader.read(buffer)) { + long capacity = integerReader.get(); + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Dynamic Table Capacity update: %d", + capacity)); + } + updateCallback.onCapacityUpdate(integerReader.get()); + reset(); + } + break; + case INSERT_NAME_LIT_NAME: + if (stringReader.read(5, buffer, nameString, maxStringLength)) { + huffmanName = stringReader.isHuffmanEncoded(); + stringReader.reset(); + state = State.INSERT_NAME_LIT_VALUE; + } + break; + case INSERT_NAME_LIT_VALUE: + int stringReaderLimit = maxStringLength > 0 ? + Math.max(maxStringLength - nameString.length(), 0) : -1; + if (stringReader.read(buffer, valueString, stringReaderLimit)) { + huffmanValue = stringReader.isHuffmanEncoded(); + // Insert with literal name instruction completely parsed + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Insert with Literal Name ('%s','%s'," + + " huffmanName='%s', huffmanValue='%s')", nameString, + valueString, huffmanName, huffmanValue)); + } + updateCallback.onInsert(nameString.toString(), valueString.toString()); + reset(); + } + break; + case INSERT_NAME_REF_NAME: + if (integerReader.read(buffer)) { + nameIndex = integerReader.get(); + state = State.INSERT_NAME_REF_VALUE; + } + break; + case INSERT_NAME_REF_VALUE: + if (stringReader.read(buffer, valueString, maxStringLength)) { + // Insert with name reference instruction completely parsed + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Insert With Name Reference (T=%d, nameIdx=%d," + + " value='%s', valueHuffman='%s')", + bitT, nameIndex, valueString, stringReader.isHuffmanEncoded())); + } + updateCallback.onInsertIndexedName(bitT == 1, nameIndex, valueString.toString()); + reset(); + } + break; + case DUPLICATE: + if (integerReader.read(buffer)) { + updateCallback.onDuplicate(integerReader.get()); + reset(); + } + break; + } + } + } + + private State identifyEncoderInstruction(ByteBuffer buffer) { + int b = buffer.get(buffer.position()) & 0xFF; // absolute read + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + case 0 -> { + // Configure integer reader to read out name index and read the T bit + integerReader.configure(6); + bitT = (b & 0b0100_0000) == 0 ? 0 : 1; + yield State.INSERT_NAME_REF_NAME; + } + case 1 -> State.INSERT_NAME_LIT_NAME; + case 2 -> { + integerReader.configure(5); + yield State.DT_CAPACITY; + } + default -> { + boolean isDuplicateInstruction = (b & 0b1110_0000) == 0; + if (isDuplicateInstruction) { + integerReader.configure(5); + yield State.DUPLICATE; + } else { + throw QPackException.encoderStreamError( + new InternalError("Unexpected encoder instruction: " + b)); + } + } + }; + } + + public void reset() { + state = State.INIT; + bitT = -1; + nameIndex = -1L; + huffmanName = false; + huffmanValue = false; + resetBuffersAndReaders(); + } + + private void resetBuffersAndReaders() { + integerReader.reset(); + stringReader.reset(); + nameString.setLength(0); + valueString.setLength(0); + } + + public interface Callback { + void onCapacityUpdate(long capacity); + + void onInsert(String name, String value); + + void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString); + + void onDuplicate(long l); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java new file mode 100644 index 00000000000..ce2da3552fa --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineIndexedPostBaseReader extends FieldLineReader { + private final IntegerReader integerReader; + private final QPACK.Logger logger; + + public FieldLineIndexedPostBaseReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.integerReader = new IntegerReader( + new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + this.logger = logger; + } + + public void configure(int b) { + integerReader.configure(4); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+---+---+---------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!integerReader.read(input)) { + return false; + } + long relativeIndex = integerReader.get(); + long absoluteIndex = prefix.base() + relativeIndex; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Post-Base Indexed Field Line: base=%s index=%s[%s]", + prefix.base(), relativeIndex, absoluteIndex)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + checkSectionSize(DynamicTable.headerSize(f)); + action.onIndexed(absoluteIndex, f.name(), f.value()); + reset(); + return true; + } + + public void reset() { + integerReader.reset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java new file mode 100644 index 00000000000..321a640bb5f --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineIndexedReader extends FieldLineReader { + private final IntegerReader integerReader; + private final QPACK.Logger logger; + + public FieldLineIndexedReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + integerReader = new IntegerReader( + new ReaderError(Http3Error.QPACK_DECOMPRESSION_FAILED, false)); + } + + public void configure(int b) { + integerReader.configure(6); + fromStaticTable = (b & 0b0100_0000) != 0; + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!integerReader.read(input)) { + return false; + } + long intValue = integerReader.get(); + // "In a field line representation, a relative index of 0 refers to the + // entry with absolute index equal to Base - 1." + long absoluteIndex = fromStaticTable ? intValue : prefix.base() - 1 - intValue; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("%s index %s", fromStaticTable ? "Static" : "Dynamic", + absoluteIndex)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + checkSectionSize(DynamicTable.headerSize(f)); + action.onIndexed(absoluteIndex, f.name(), f.value()); + reset(); + return true; + } + + public void reset() { + integerReader.reset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java new file mode 100644 index 00000000000..1dfa81b5631 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.DynamicTable.ENTRY_SIZE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineLiteralsReader extends FieldLineReader { + private boolean hideIntermediary; + private boolean huffmanName, huffmanValue; + private final StringBuilder name, value; + private final StringReader stringReader; + private final QPACK.Logger logger; + private boolean firstValueRead = false; + + public FieldLineLiteralsReader(long maxSectionSize, AtomicLong sectionSizeTracker, + QPACK.Logger logger) { + // Dynamic table is not needed for literals reader + super(null, maxSectionSize, sectionSizeTracker); + this.logger = logger; + stringReader = new StringReader(new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + name = new StringBuilder(512); + value = new StringBuilder(1024); + } + + public void configure(int b) { + hideIntermediary = (b & 0b0001_0000) != 0; + } + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | N | H |NameLen(3+)| + // +---+---+-----------------------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + long readPart = ENTRY_SIZE + name.length() + value.length(); + checkPartialSize(readPart); + return false; + } + String n = name.toString(); + String v = value.toString(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with literal name ('%s', huffman=%b, '%s', huffman=%b)", + n, huffmanName, v, huffmanValue)); + } + checkSectionSize(DynamicTable.headerSize(n, v)); + action.onLiteralWithLiteralName(n, huffmanName, v, huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!stringReader.read(3, input, name, getMaxFieldLineLimit(name.length()))) { + return false; + } + huffmanName = stringReader.isHuffmanEncoded(); + stringReader.reset(); + firstValueRead = true; + return false; + } else { + int maxLength = getMaxFieldLineLimit(name.length() + value.length()); + if (!stringReader.read(input, value, maxLength)) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + return true; + } + + public void reset() { + name.setLength(0); + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java new file mode 100644 index 00000000000..420844ad72d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineNameRefPostBaseReader extends FieldLineReader { + private long intValue; + private boolean hideIntermediary; + private boolean huffmanValue; + private final StringBuilder value; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final QPACK.Logger logger; + + private boolean firstValueRead = false; + + FieldLineNameRefPostBaseReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + var errorToReport = new ReaderError(QPACK_DECOMPRESSION_FAILED, false); + integerReader = new IntegerReader(errorToReport); + stringReader = new StringReader(errorToReport); + value = new StringBuilder(1024); + } + + public void configure(int b) { + hideIntermediary = (b & 0b0000_1000) != 0; + integerReader.configure(3); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---+-----------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + if (firstValueRead) { + long readPart = DynamicTable.ENTRY_SIZE + value.length(); + checkPartialSize(readPart); + } + return false; + } + + long absoluteIndex = prefix.base() + intValue; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with post-base name reference (%s, %s, '%s', huffman=%b)", + absoluteIndex, prefix.base(), value, huffmanValue)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + String valueStr = value.toString(); + checkSectionSize(DynamicTable.headerSize(f.name(), valueStr)); + action.onLiteralWithNameReference(absoluteIndex, + f.name(), valueStr, huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value, getMaxFieldLineLimit())) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + + return true; + } + + public void reset() { + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java new file mode 100644 index 00000000000..f7e3d300628 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineNameReferenceReader extends FieldLineReader { + private long intValue; + private boolean hideIntermediary; + private boolean huffmanValue; + private final StringBuilder value; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final QPACK.Logger logger; + + private boolean firstValueRead = false; + + FieldLineNameReferenceReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + var errorToReport = new ReaderError(QPACK_DECOMPRESSION_FAILED, false); + integerReader = new IntegerReader(errorToReport); + stringReader = new StringReader(errorToReport); + value = new StringBuilder(1024); + } + + public void configure(int b) { + fromStaticTable = (b & 0b0001_0000) != 0; + hideIntermediary = (b & 0b0010_0000) != 0; + integerReader.configure(4); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T | NameIndex (4+)| + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + if (firstValueRead) { + long readPart = DynamicTable.ENTRY_SIZE + value.length(); + checkPartialSize(readPart); + } + return false; + } + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with name reference (%s, %s, '%s', huffman=%b)", + fromStaticTable ? "static" : "dynamic", intValue, value, huffmanValue)); + } + long absoluteIndex = fromStaticTable ? intValue : prefix.base() - 1 - intValue; + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + String valueStr = value.toString(); + checkSectionSize(DynamicTable.headerSize(f.name(), valueStr)); + action.onLiteralWithNameReference(absoluteIndex, f.name(), valueStr, + huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value, getMaxFieldLineLimit())) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + + return true; + } + + public void reset() { + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java new file mode 100644 index 00000000000..da67f92bf19 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.StaticTable; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +sealed abstract class FieldLineReader permits FieldLineIndexedPostBaseReader, + FieldLineIndexedReader, FieldLineLiteralsReader, FieldLineNameRefPostBaseReader, + FieldLineNameReferenceReader { + + final long maxSectionSize; + boolean fromStaticTable; + private final AtomicLong sectionSizeTracker; + private final DynamicTable dynamicTable; + + FieldLineReader(DynamicTable dynamicTable, long maxSectionSize, AtomicLong sectionSizeTracker) { + this.maxSectionSize = maxSectionSize; + this.sectionSizeTracker = sectionSizeTracker; + this.dynamicTable = dynamicTable; + } + + abstract void reset(); + abstract void configure(int b); + abstract boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action); + + final void checkSectionSize(long fieldSize) { + long sectionSize = sectionSizeTracker.addAndGet(fieldSize); + if (maxSectionSize > 0 && sectionSize > maxSectionSize) { + throw maxFieldSectionExceeded(sectionSize, maxSectionSize); + } + } + + final void checkPartialSize(long partialFieldSize) { + long sectionSize = sectionSizeTracker.get() + partialFieldSize; + if (maxSectionSize > 0 && sectionSize > maxSectionSize) { + throw maxFieldSectionExceeded(sectionSize, maxSectionSize); + } + } + + final int getMaxFieldLineLimit(int partiallyRead) { + int maxLimit = -1; + if (maxSectionSize > 0) { + maxLimit = Math.clamp(maxSectionSize - partiallyRead - 32 - + sectionSizeTracker.get(), 0, Integer.MAX_VALUE); + } + return maxLimit; + } + + final int getMaxFieldLineLimit() { + return getMaxFieldLineLimit(0); + } + + private static QPackException maxFieldSectionExceeded(long sectionSize, long maxSize) { + throw QPackException.decompressionFailed( + new ProtocolException("Size exceeds MAX_FIELD_SECTION_SIZE: %s > %s" + .formatted(sectionSize, maxSize)), false); + } + + /** + * Checks if the decoder encounters a reference in a field line representation to + * a dynamic table entry that has already been evicted or that has an absolute index + * greater than or equal to the declared Required Insert Count (Section 4.5.1), + * it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. + * @param absoluteIndex dynamic table absolute index + * @param prefix field line section prefix + */ + void checkEntryIndex(long absoluteIndex, FieldSectionPrefix prefix) { + if (!fromStaticTable && absoluteIndex >= prefix.requiredInsertCount()) { + throw QPackException.decompressionFailed( + new IOException("header index is greater than RIC"), true); + } + } + + /** + * Return a header field entry for the specified entry index. The table type + * is selected according to the {@code fromStaticTable} value. + * @param index absolute index of the table entry. + * @return a header field corresponding to the specified entry + */ + final HeaderField entryAtIndex(long index) { + HeaderField f; + try { + if (fromStaticTable) { + f = StaticTable.HTTP3.get(index); + } else { + assert dynamicTable != null; + f = dynamicTable.get(index); + } + } catch (IndexOutOfBoundsException | IllegalStateException | IllegalArgumentException e) { + throw QPackException.decompressionFailed( + new IOException("header fields table index", e), true); + } + return f; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java new file mode 100644 index 00000000000..a4ea55661fe --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +public class HeaderFrameReader { + + private enum State { + // Nothing has been read so-far, "Required Insert Count" (RIC) will be read next + INITIAL, + // "Required Insert Count" read is done, "S" and "Delta Base" are next + DELTA_BASE, + // Encoded Field Section Prefix read is done, ready to start reading header fields. + // In this state we only select a proper reader based on the field line encoding type, + // ie the first byte is analysed to select a proper reader. + SELECT_FIELD_READER, + INDEXED, + INDEX_WITH_POST_BASE, + LITERAL_WITH_LITERAL_NAME, + LITERAL_WITH_NAME_REF, + LITERAL_WITH_POST_BASE, + AWAITING_DT_INSERT_COUNT + } + + /* + 4.5.1. Encoded Field Section Prefix + Each encoded field section is prefixed with two integers. The Required Insert Count + is encoded as an integer with an 8-bit prefix using the encoding described in Section 4.5.1.1. + The Base is encoded as a Sign bit ('S') and a Delta Base value with a 7-bit prefix; + see Section 4.5.1.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | Required Insert Count (8+) | + +---+---------------------------+ + | S | Delta Base (7+) | + +---+---------------------------+ + */ + long requiredInsertCount; + long deltaBase; + int signBit; + volatile FieldSectionPrefix fieldSectionPrefix; + private final IntegerReader integerReader; + private FieldLineReader reader; + private final QPACK.Logger logger; + private final FieldLineIndexedReader indexedReader; + private final FieldLineIndexedPostBaseReader indexedPostBaseReader; + private final FieldLineNameReferenceReader literalWithNameReferenceReader; + private final FieldLineNameRefPostBaseReader literalWithNameRefPostBaseReader; + + private final FieldLineLiteralsReader literalWithLiteralNameReader; + // Need dynamic table reference for decoding field line section prefix + private final DynamicTable dynamicTable; + private final DecodingCallback decodingCallback; + + private volatile State state = State.INITIAL; + + private final SequentialScheduler headersScheduler = SequentialScheduler.lockingScheduler(this::readLoop); + private final ConcurrentLinkedQueue headersData = new ConcurrentLinkedQueue<>(); + + private final AtomicLong blockedStreamsCounter; + private final long maxBlockedStreams; + + // A tracker of header data received by the decoder, to check that the peer encoder + // honours the SETTINGS_MAX_FIELD_SECTION_SIZE value: + // RFC-9114: 4.2.2. Header Size Constraints + // "If an implementation wishes to advise its peer of this limit, it can + // be conveyed as a number of bytes in the SETTINGS_MAX_FIELD_SECTION_SIZE parameter. + // An implementation that has received this parameter SHOULD NOT send an HTTP message + // header that exceeds the indicated size" + // "A client can discard responses that it cannot process." + // + // Maximum allowed value is passed to FieldLineReader's implementations and not stored in + // HeaderFrameReader instance. + private final AtomicLong fieldSectionSizeTracker; + + private static final AtomicLong HEADER_FRAME_READER_IDS = new AtomicLong(); + + private void readLoop() { + try { + readLoop0(); + } catch (QPackException qPackException) { + Throwable cause = qPackException.getCause(); + if (qPackException.isConnectionError()) { + decodingCallback.onConnectionError(cause, qPackException.http3Error()); + } else { + decodingCallback.onStreamError(cause, qPackException.http3Error()); + } + } catch (Throwable throwable) { + decodingCallback.onConnectionError(throwable, H3_INTERNAL_ERROR); + } finally { + // Stop the scheduler, clear the reader's queue and + // remove all insert count notification events associated + // with current stream. + if (decodingCallback.hasError()) { + headersScheduler.stop(); + headersData.clear(); + dynamicTable.cleanupStreamInsertCountNotifications(decodingCallback.streamId()); + } + } + } + + private void readLoop0() { + ByteBuffer headerBlock; + OUTER: + while (!decodingCallback.hasError() && (headerBlock = headersData.peek()) != null) { + boolean endOfHeaderBlock = headerBlock == QuicStreamReader.EOF; + State state = this.state; + FieldSectionPrefix sectionPrefix = this.fieldSectionPrefix; + while (!decodingCallback.hasError() && headerBlock.hasRemaining()) { + if (state == State.SELECT_FIELD_READER) { + int b = headerBlock.get(headerBlock.position()) & 0xff; // absolute read + state = this.state = selectHeaderReaderState(b); + if (logger.isLoggable(EXTRA)) { + String message = format("next binary representation %s (first byte 0x%02x)", state, b); + logger.log(EXTRA, () -> message); + } + reader = switch (state) { + case INDEXED -> indexedReader; + case LITERAL_WITH_NAME_REF -> literalWithNameReferenceReader; + case LITERAL_WITH_LITERAL_NAME -> literalWithLiteralNameReader; + case INDEX_WITH_POST_BASE -> indexedPostBaseReader; + case LITERAL_WITH_POST_BASE -> literalWithNameRefPostBaseReader; + default -> throw QPackException.decompressionFailed( + new InternalError("Unexpected decoder state: " + state), false); + }; + reader.configure(b); + } else if (state == State.INITIAL) { + if (!integerReader.read(headerBlock)) { + continue; + } + // Required Insert Count was fully read + requiredInsertCount = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Encoded Required Insert Count = %d", requiredInsertCount)); + } + // Continue reading S and Delta Base values + state = this.state = State.DELTA_BASE; + // Reset integer reader + integerReader.reset(); + // Prepare it for reading S and Delta Base (7+) + integerReader.configure(7); + continue; + } else if (state == State.DELTA_BASE) { + if (signBit == -1) { + int b = headerBlock.get(headerBlock.position()) & 0xff; // absolute read + signBit = (b & 0b1000_0000) == 0b1000_0000 ? 1 : 0; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Base Sign = %d", signBit)); + } + } + if (!integerReader.read(headerBlock)) { + continue; + } + deltaBase = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Delta Base = %d", deltaBase)); + } + // Construct field section prefix from the parsed fields + sectionPrefix = this.fieldSectionPrefix = + FieldSectionPrefix.decode(requiredInsertCount, deltaBase, + signBit, dynamicTable); + + // Check if decoding of field section is blocked due to not yet received + // dynamic table entries + long insertCount = dynamicTable.insertCount(); + if (sectionPrefix.requiredInsertCount() > insertCount) { + long blocked = blockedStreamsCounter.incrementAndGet(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> "Blocked stream observed. Total blocked: " + blocked + + " Max allowed: " + maxBlockedStreams); + } + // System property value is checked here instead of the HTTP3 settings because decoder uses its + // value to update connection settings. HTTP client's encoder implementation won't block the streams - + // only acknowledged entry references is used, therefore this connection setting is not consulted + // on encoder side. + if (blocked > maxBlockedStreams) { + var ioException = new IOException(("too many blocked streams: current=%d; max=%d; " + + "prefixCount=%d; tableCount=%d").formatted(blocked, maxBlockedStreams, + sectionPrefix.requiredInsertCount(), insertCount)); + // If a decoder encounters more blocked streams than it promised to support, + // it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. + throw QPackException.decompressionFailed(ioException, true); + } else { + CompletableFuture future = + dynamicTable.awaitFutureInsertCount(decodingCallback.streamId(), + sectionPrefix.requiredInsertCount()); + state = this.state = State.AWAITING_DT_INSERT_COUNT; + future.thenRun(this::onInsertCountUpdate); + } + break OUTER; + } + // The stream is unblocked - field lines can be decoded now + state = this.state = State.SELECT_FIELD_READER; + continue; + } else if (state == State.AWAITING_DT_INSERT_COUNT) { + // If we're waiting for a specific dynamic table update + return; + } + if (reader.read(headerBlock, sectionPrefix, decodingCallback)) { + // Finished reading of one header field line + state = this.state = State.SELECT_FIELD_READER; + } + } + if (!headerBlock.hasRemaining()) { + var head = headersData.poll(); + assert head == headerBlock; + } + if (endOfHeaderBlock) { + if (state == State.SELECT_FIELD_READER) { + decodingCallback.onComplete(); + } else { + logger.log(NORMAL, () -> "unexpected end of representation"); + throw QPackException.decompressionFailed( + new ProtocolException("Unexpected end of header block"), true); + } + } + } + } + + private void onInsertCountUpdate() { + long blocked = blockedStreamsCounter.decrementAndGet(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> "Stream Unblocked - number of blocked streams: " + blocked); + } + state = State.SELECT_FIELD_READER; + headersScheduler.runOrSchedule(); + } + + public HeaderFrameReader(DynamicTable dynamicTable, DecodingCallback callback, + AtomicLong blockedStreamsCounter, long maxBlockedStreams, + long maxFieldSectionSize, QPACK.Logger logger) { + this.blockedStreamsCounter = blockedStreamsCounter; + this.logger = logger.subLogger("HeaderFrameReader#" + + HEADER_FRAME_READER_IDS.incrementAndGet()); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("New HeaderFrameReader, dynamic table capacity = %s", + dynamicTable.capacity())); + /* To correlate with logging outside QPACK, knowing + hashCode/toString is important */ + logger.log(NORMAL, () -> { + String hashCode = Integer.toHexString(System.identityHashCode(this)); + return format("toString='%s', identityHashCode=%s", this, hashCode); + }); + } + this.fieldSectionSizeTracker = new AtomicLong(); + indexedReader = new FieldLineIndexedReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineIndexedReader")); + indexedPostBaseReader = new FieldLineIndexedPostBaseReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineIndexedPostBaseReader")); + literalWithNameReferenceReader = new FieldLineNameReferenceReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineNameReferenceReader")); + literalWithNameRefPostBaseReader = new FieldLineNameRefPostBaseReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineNameRefPostBaseReader")); + literalWithLiteralNameReader = new FieldLineLiteralsReader( + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineLiteralsReader")); + integerReader = new IntegerReader(new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + resetPrefixVars(); + // Since reader is constructed in Initial state - it means that the + // "Required Insert Count" will be read first. + integerReader.configure(8); + decodingCallback = callback; + this.dynamicTable = dynamicTable; + this.maxBlockedStreams = maxBlockedStreams; + } + + private void resetPrefixVars() { + requiredInsertCount = -1L; + deltaBase = -1L; + signBit = -1; + fieldSectionPrefix = null; + fieldSectionSizeTracker.set(0); + } + + public FieldSectionPrefix decodedSectionPrefix() { + if (deltaBase == -1L) { + throw new IllegalStateException("Field Section Prefix not parsed yet"); + } + return fieldSectionPrefix; + } + + public void read(ByteBuffer headerBlock, boolean endOfHeaderBlock) { + requireNonNull(headerBlock, "headerBlock"); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("reading %s, end of header block? %s", + headerBlock, endOfHeaderBlock)); + } + headersData.add(headerBlock); + if (endOfHeaderBlock) { + headersData.add(QuicStreamReader.EOF); + } + headersScheduler.runOrSchedule(); + } + + private State selectHeaderReaderState(int b) { + // First non-zero bit in lower 8 bits (see the caller) + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Index (6+) | + +---+---+-----------------------+ + */ + case 0 -> State.INDEXED; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | N | T |Name Index (4+)| + +---+---+---+---+---------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + case 1 -> State.LITERAL_WITH_NAME_REF; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | N | H |NameLen(3+)| + +---+---+---+---+---+-----------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + case 2 -> State.LITERAL_WITH_LITERAL_NAME; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 1 | Index (4+) | + +---+---+---+---+---------------+ + */ + case 3 -> State.INDEX_WITH_POST_BASE; + // "Literal Field Line with Post-Base Name Reference": + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---------------+ + default -> { + if ((b & 0xF0) == 0) { + yield State.LITERAL_WITH_POST_BASE; + } + throw QPackException.decompressionFailed( + new IOException("Unknown frame reader line prefix: " + b), + false); + } + }; + } + + /** + * Reset the state of the HeaderFrameReader so that it's ready + * to parse a new HeaderFrame. + */ + public void reset() { + state = State.INITIAL; + reader = null; + resetPrefixVars(); + integerReader.reset(); + integerReader.configure(8); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java new file mode 100644 index 00000000000..35025c3f06a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static java.lang.String.format; + +/** + * This class able to decode integers up to and including 62 bits long values. + * https://www.rfc-editor.org/rfc/rfc9204.html#name-prefixed-integers + */ +public final class IntegerReader { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_READ = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int N; + private long maxValue; + private long value; + private long r; + private long b = 1; + private final ReaderError readError; + + public IntegerReader(ReaderError readError) { + this.readError = readError; + } + + public IntegerReader() { + this(new ReaderError(Http3Error.H3_INTERNAL_ERROR, true)); + } + + // "QPACK implementations MUST be able to decode integers up to and including 62 bits long." + // https://www.rfc-editor.org/rfc/rfc9204.html#name-prefixed-integers + public static final long QPACK_MAX_INTEGER_VALUE = (1L << 62) - 1; + + public IntegerReader configure(int N) { + return configure(N, QPACK_MAX_INTEGER_VALUE); + } + + // + // Why is it important to configure 'maxValue' here. After all we can wait + // for the integer to be fully read and then check it. Can't we? + // + // Two reasons. + // + // 1. Value wraps around long won't be unnoticed. + // 2. It can spit out an exception as soon as it becomes clear there's + // an overflow. Therefore, no need to wait for the value to be fully read. + // + public IntegerReader configure(int N, long maxValue) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + checkPrefix(N); + if (maxValue < 0) { + throw new IllegalArgumentException( + "maxValue >= 0: maxValue=" + maxValue); + } + this.maxValue = maxValue; + this.N = N; + state = CONFIGURED; + return this; + } + + public boolean read(ByteBuffer input) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + int n = input.get() & max; + if (n != max) { + value = n; + state = DONE; + return true; + } else { + r = max; + } + state = FIRST_BYTE_READ; + } + if (state == FIRST_BYTE_READ) { + try { + // variable-length quantity (VLQ) + byte i; + boolean continuationFlag; + do { + if (!input.hasRemaining()) { + return false; + } + i = input.get(); + // RFC 7541: 5.1. Integer Representation + // "The most significant bit of each octet is used + // as a continuation flag: its value is set to 1 except + // for the last octet in the list" + continuationFlag = (i & 0b10000000) != 0; + long increment = Math.multiplyExact(b, i & 127); + if (continuationFlag) { + b = Math.multiplyExact(b, 128); + } + if (r > maxValue - increment) { + throw readError.toQPackException( + new IOException(format( + "Integer overflow: maxValue=%,d, value=%,d", + maxValue, r + increment))); + } + r += increment; + } while (continuationFlag); + value = r; + state = DONE; + return true; + } catch (ArithmeticException arithmeticException) { + // Sequence of bytes encodes value greater + // than QPACK_MAX_INTEGER_VALUE + throw readError.toQPackException(new IOException("Integer overflow", + arithmeticException)); + } + } + throw new InternalError(Arrays.toString( + new Object[]{state, N, maxValue, value, r, b})); + } + + public long get() throws IllegalStateException { + if (state != DONE) { + throw new IllegalStateException("Has not been fully read yet"); + } + return value; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerReader reset() { + b = 1; + state = NEW; + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java new file mode 100644 index 00000000000..3be6033f827 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.QPackException; + +/** + * QPack readers configuration record to be used by the readers to + * report errors. + * @param http3Error corresponding HTTP/3 error code. + * @param isConnectionError if the reader error should be treated + * as connection error. + */ +record ReaderError(Http3Error http3Error, boolean isConnectionError) { + + /** + * Construct a {@link QPackException} from on {@code http3Error}, + * {@code isConnectionError} and provided {@code "cause"} values. + * @param cause cause of the constructed {@link QPackException} + * @return a {@code QPackException} instance. + */ + QPackException toQPackException(Throwable cause) { + return new QPackException(http3Error, cause, isConnectionError); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java new file mode 100644 index 00000000000..0cd0c92e91a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.hpack.ISO_8859_1; +import jdk.internal.net.http.hpack.Huffman; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.http3.Http3Error; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +public final class StringReader { + + private static final int NEW = 0; + private static final int FIRST_BYTE_READ = 1; + private static final int LENGTH_READ = 2; + private static final int DONE = 4; + + private final ReaderError readError; + private final IntegerReader intReader; + private final Huffman.Reader huffmanReader = new QuickHuffman.Reader(); + private final ISO_8859_1.Reader plainReader = new ISO_8859_1.Reader(); + + private int state = NEW; + private boolean huffman; + private int remainingLength; + + public StringReader() { + this(new ReaderError(Http3Error.H3_INTERNAL_ERROR, true)); + } + + public StringReader(ReaderError readError) { + this.readError = readError; + this.intReader = new IntegerReader(readError); + } + + public boolean read(ByteBuffer input, Appendable output, int maxLength) { + return read(7, input, output, maxLength); + } + + boolean read(int N, ByteBuffer input, Appendable output, int maxLength) { + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == NEW) { + int huffmanBit = switch (N) { + case 7 -> 0b1000_0000; // for all value strings + case 5 -> 0b0010_0000; // in name string for insert literal + case 3 -> 0b0000_1000; // in name string for literal + default -> throw new IllegalStateException("Unexpected value: " + N); + }; + int p = input.position(); + huffman = (input.get(p) & huffmanBit) != 0; + state = FIRST_BYTE_READ; + intReader.configure(N); + } + if (state == FIRST_BYTE_READ) { + boolean lengthRead = intReader.read(input); + if (!lengthRead) { + return false; + } + long remainingLengthLong = intReader.get(); + if (maxLength >= 0) { + long huffmanEstimate = huffman ? + remainingLengthLong / 4 : remainingLengthLong; + if (huffmanEstimate > maxLength) { + throw readError.toQPackException(new ProtocolException( + "Size exceeds MAX_FIELD_SECTION_SIZE or dynamic table capacity.")); + } + } + remainingLength = (int) remainingLengthLong; + state = LENGTH_READ; + } + if (state == LENGTH_READ) { + boolean isLast = input.remaining() >= remainingLength; + int oldLimit = input.limit(); + if (isLast) { + input.limit(input.position() + remainingLength); + } + remainingLength -= Math.min(input.remaining(), remainingLength); + try { + if (huffman) { + huffmanReader.read(input, output, isLast); + } else { + plainReader.read(input, output); + } + } catch (IOException ioe) { + throw readError.toQPackException(ioe); + } + if (isLast) { + input.limit(oldLimit); + state = DONE; + } + return isLast; + } + throw new InternalError(Arrays.toString( + new Object[]{state, huffman, remainingLength})); + } + + public boolean isHuffmanEncoded() { + if (state < FIRST_BYTE_READ) { + throw new IllegalStateException("Has not been fully read yet"); + } + return huffman; + } + + public void reset() { + if (huffman) { + huffmanReader.reset(); + } else { + plainReader.reset(); + } + intReader.reset(); + state = NEW; + } +} diff --git a/test/jdk/sun/tools/jrunscript/CheckEngine.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java similarity index 67% rename from test/jdk/sun/tools/jrunscript/CheckEngine.java rename to src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java index dd8f4cd81ae..b028b994df2 100644 --- a/test/jdk/sun/tools/jrunscript/CheckEngine.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -20,17 +22,12 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ +package jdk.internal.net.http.qpack.writers; -import javax.script.*; +import java.nio.ByteBuffer; -/* - * If the JDK being tested is not a Sun product JDK and a js - * engine is not present, return an exit code of 2 to indicate that - * the jrunscript tests which assume a js engine can be vacuously - * passed. - */ -public class CheckEngine { - public static void main(String... args) { - System.exit(2); - } +interface BinaryRepresentationWriter { + boolean write(ByteBuffer destination); + + BinaryRepresentationWriter reset(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java new file mode 100644 index 00000000000..c27f46e2761 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class DecoderInstructionsWriter { + private final QPACK.Logger logger; + private boolean encoding; + private static final AtomicLong IDS = new AtomicLong(); + + private final IntegerWriter integerWriter = new IntegerWriter(); + + public DecoderInstructionsWriter() { + long id = IDS.incrementAndGet(); + this.logger = QPACK.getLogger().subLogger("DecoderInstructionsWriter#" + id); + } + + /* + * Configure the writer for encoding "Section Acknowledgment" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 1 | Stream ID (7+) | + * +---+---------------------------+ + */ + public int configureForSectionAck(long streamId) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(streamId, 7, 0b1000_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Section Acknowledgment for stream id=%s", + streamId)); + } + return IntegerWriter.requiredBufferSize(7, streamId); + } + + /* + * Configure the writer for encoding "Stream Cancellation" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 1 | Stream ID (6+) | + * +---+---+-----------------------+ + */ + public int configureForStreamCancel(long streamId) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(streamId, 6, 0b0100_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Stream Cancellation for stream id=%s", + streamId)); + } + return IntegerWriter.requiredBufferSize(6, streamId); + } + + /* + * Configure the writer for encoding "Insert Count Increment" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | Increment (6+) | + * +---+---+-----------------------+ + */ + public int configureForInsertCountInc(long increment) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(increment, 6, 0b0000_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Insert Count Increment value=%s", + increment)); + } + return IntegerWriter.requiredBufferSize(6, increment); + } + + public boolean write(ByteBuffer byteBuffer) { + if (!encoding) { + throw new IllegalStateException("Writer hasn't been configured"); + } + boolean done = integerWriter.write(byteBuffer); + if (done) { + integerWriter.reset(); + encoding = false; + } + return done; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java new file mode 100644 index 00000000000..29f6cac9ed8 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +final class EncoderDuplicateEntryWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter; + + public EncoderDuplicateEntryWriter() { + this.intWriter = new IntegerWriter(); + } + + public EncoderDuplicateEntryWriter configure(long relativeId) { + // IntegerWriter.configure checks if the relative id value is not negative + intWriter.configure(relativeId, 5, 0b0000_0000); + // Need to store entry id for adding a duplicate to the dynamic table + // once write operation is completed + return this; + } + + @Override + public boolean write(ByteBuffer destination) { + // IntegerWriter.write checks if it was properly configured + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java new file mode 100644 index 00000000000..1a920ff5b7b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +final class EncoderDynamicTableCapacityWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter; + + public EncoderDynamicTableCapacityWriter() { + this.intWriter = new IntegerWriter(); + } + + public EncoderDynamicTableCapacityWriter configure(long capacity) { + // IntegerWriter.configure checks if the capacity value is not negative + intWriter.configure(capacity, 5, 0b0010_0000); + return this; + } + + @Override + public boolean write(ByteBuffer destination) { + // IntegerWriter.write checks if it was properly configured + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java new file mode 100644 index 00000000000..d1851a0c204 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +class EncoderInsertIndexedNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + public EncoderInsertIndexedNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Encoder Insert With %s Table Name Reference (%s, '%s', huffman=%b)", + e.isStaticTable() ? "Static" : "Dynamic", e.index(), e.value(), e.huffmanValue())); + } + return this.index(e).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private EncoderInsertIndexedNameWriter index(TableEntry e) { + int N = 6; + int payload = 0b1000_0000; + long index = e.index(); + if (e.isStaticTable()) { + payload |= 0b0100_0000; + } + intWriter.configure(index, N, payload); + return this; + } + + private EncoderInsertIndexedNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java new file mode 100644 index 00000000000..1783f60b062 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class EncoderInsertLiteralNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + EncoderInsertLiteralNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Insert With Literal Name (%s, '%s', huffmanName=%b, huffmanValue=%b)", + e.name(), e.value(), e.huffmanName(), e.huffmanValue())); + } + return this.name(e).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!nameWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + nameWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private EncoderInsertLiteralNameWriter name(TableEntry e) { + int N = 5; + int payload = 0b0100_0000; + if (e.huffmanName()) { + payload |= 0b0010_0000; + } + nameWriter.configure(e.name(), N, payload, e.huffmanName()); + return this; + } + + private EncoderInsertLiteralNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java new file mode 100644 index 00000000000..c4a22436083 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class EncoderInstructionsWriter { + private BinaryRepresentationWriter writer; + private final QPACK.Logger logger; + private final EncoderInsertIndexedNameWriter insertIndexedNameWriter; + private final EncoderInsertLiteralNameWriter insertLiteralNameWriter; + private final EncoderDuplicateEntryWriter duplicateWriter; + private final EncoderDynamicTableCapacityWriter capacityWriter; + private boolean encoding; + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + public EncoderInstructionsWriter() { + this(QPACK.getLogger()); + } + + public EncoderInstructionsWriter(QPACK.Logger parentLogger) { + long id = ENCODERS_IDS.incrementAndGet(); + this.logger = parentLogger.subLogger("EncoderInstructionsWriter#" + id); + // Writer for "Insert with Name Reference" encoder instruction + insertIndexedNameWriter = new EncoderInsertIndexedNameWriter( + logger.subLogger("EncoderInsertIndexedNameWriter")); + // Writer for "Insert with Literal Name" encoder instruction + insertLiteralNameWriter = new EncoderInsertLiteralNameWriter( + logger.subLogger("EncoderInsertLiteralNameWriter")); + // Writer for "Set Dynamic Table Capacity" encoder instruction + capacityWriter = new EncoderDynamicTableCapacityWriter(); + // Writer for "Duplicate" encoder instruction + duplicateWriter = new EncoderDuplicateEntryWriter(); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Insert with Name Reference" or "Insert with Literal Name" + * encoder instruction. The instruction is selected based on TableEntry.type() value: + * "Insert with Name Reference" is selected for TableEntry.EntryType.NAME: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 1 | T | Name Index (6+) | + * +---+---+-----------------------+ + * | H | Value Length (7+) | + * +---+---------------------------+ + * | Value String (Length bytes) | + * +-------------------------------+ + * + * "Insert with Literal Name" is selected for TableEntry.EntryType.NEITHER: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 1 | H | Name Length (5+) | + * +---+---+---+-------------------+ + * | Name String (Length bytes) | + * +---+---------------------------+ + * | H | Value Length (7+) | + * +---+---------------------------+ + * | Value String (Length bytes) | + * +-------------------------------+ + */ + public int configureForEntryInsertion(TableEntry e) { + checkIfEncodingInProgress(); + encoding = true; + writer = switch (e.type()) { + case NAME -> insertIndexedNameWriter.configure(e); + case NEITHER -> insertLiteralNameWriter.configure(e); + default -> throw new IllegalArgumentException("Unsupported table entry insertion type: " + e.type()); + }; + return calculateEntryInsertionSize(e); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Duplicate" encoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | 0 | Index (5+) | + * +---+---+---+-------------------+ + */ + public int configureForEntryDuplication(long entryIndexToDuplicate) { + checkIfEncodingInProgress(); + encoding = true; + duplicateWriter.configure(entryIndexToDuplicate); + writer = duplicateWriter; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("duplicate entry with id=%s", entryIndexToDuplicate)); + } + return IntegerWriter.requiredBufferSize(5, entryIndexToDuplicate); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Set Dynamic Table Capacity" encoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | 1 | Capacity (5+) | + * +---+---+---+-------------------+ + */ + public int configureForTableCapacityUpdate(long tableCapacity) { + checkIfEncodingInProgress(); + encoding = true; + capacityWriter.configure(tableCapacity); + writer = capacityWriter; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("set dynamic table capacity to %s", tableCapacity)); + } + return IntegerWriter.requiredBufferSize(5, tableCapacity); + } + + + public boolean write(ByteBuffer byteBuffer) { + if (!encoding) { + throw new IllegalStateException("Writer hasn't been configured"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", byteBuffer)); + } + boolean done = writer.write(byteBuffer); + if (done) { + writer.reset(); + encoding = false; + } + return done; + } + + private int calculateEntryInsertionSize(TableEntry e) { + int vlen = Math.min(QuickHuffman.lengthOf(e.value()), e.value().length()); + int integerValuesSize; + return switch (e.type()) { + case NAME -> { + // Calculate how many bytes are needed to encode the index part: + // | 1 | T | Name Index (6+) | + integerValuesSize = IntegerWriter.requiredBufferSize(6, e.index()); + // Calculate how many bytes are needed to encode the value length part: + // | H | Value Length (7+) | + integerValuesSize += IntegerWriter.requiredBufferSize(7, vlen); + // We also need vlen bytes for the value string content + yield integerValuesSize + vlen; + } + case NEITHER -> { + int nlen = Math.min(QuickHuffman.lengthOf(e.name()), e.name().length()); + // Calculate how many bytes are needed to encode the name length part: + // | 0 | 1 | H | Name Length (5+) | + integerValuesSize = IntegerWriter.requiredBufferSize(5, nlen); + // Calculate how many bytes are needed to encode the value length part: + // | H | Value Length (7+) | + integerValuesSize += IntegerWriter.requiredBufferSize(7, vlen); + // We also need nlen + vlen bytes for the name and the value strings + // content + yield integerValuesSize + nlen + vlen; + } + default -> throw new IllegalArgumentException("Unsupported table entry type: " + e.type()); + }; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java new file mode 100644 index 00000000000..6e268f32acc --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class FieldLineIndexedNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + FieldLineIndexedNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, boolean hideIntermediary, long base) + throws IndexOutOfBoundsException { + return e.isStaticTable() ? configureStatic(e, hideIntermediary) : + configureDynamic(e, hideIntermediary, base); + } + + private BinaryRepresentationWriter configureStatic(TableEntry e, boolean hideIntermediary) + throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With Static Table Name Reference" + + " (%s, '%s', huffman=%b, hideIntermediary=%b)", + e.index(), e.value(), e.huffmanValue(), hideIntermediary)); + } + return this.staticIndex(e.index(), hideIntermediary).value(e); + } + + private BinaryRepresentationWriter configureDynamic(TableEntry e, boolean hideIntermediary, long base) + throws IndexOutOfBoundsException { + boolean usePostBase = e.index() >= base; + long index = usePostBase ? e.index() - base : base - 1 - e.index(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With %s Dynamic Table Name Reference" + + " (%s, '%s', huffman=%b, hideIntermediary=%b)", + usePostBase ? "Post-Base" : "", index, e.value(), e.huffmanValue(), + hideIntermediary)); + } + if (usePostBase) { + return this.dynamicPostBaseIndex(index, hideIntermediary).value(e); + } else { + return this.dynamicIndex(index, hideIntermediary).value(e); + } + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public FieldLineIndexedNameWriter reset() { + intWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private FieldLineIndexedNameWriter staticIndex(long absoluteIndex, boolean hideIntermediary) { + int payload = 0b0101_0000; + if (hideIntermediary) { + payload |= 0b0010_0000; + } + intWriter.configure(absoluteIndex, 4, payload); + return this; + } + + private FieldLineIndexedNameWriter dynamicIndex(long relativeIndex, boolean hideIntermediary) { + int payload = 0b0100_0000; + if (hideIntermediary) { + payload |= 0b0010_0000; + } + intWriter.configure(relativeIndex, 4, payload); + return this; + } + + private FieldLineIndexedNameWriter dynamicPostBaseIndex(long relativeIndex, boolean hideIntermediary) { + int payload = 0b0000_0000; + if (hideIntermediary) { + payload |= 0b0000_1000; + } + intWriter.configure(relativeIndex, 3, payload); + return this; + } + + private FieldLineIndexedNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java new file mode 100644 index 00000000000..c829966980a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class FieldLineIndexedWriter implements BinaryRepresentationWriter { + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + + public FieldLineIndexedWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, long base) { + return e.isStaticTable() ? configureStatic(e) : configureDynamic(e, base); + } + + private BinaryRepresentationWriter configureStatic(TableEntry e) { + assert e.isStaticTable(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Indexed Field Line Static Table reference" + + " (%s, '%s', '%s')", e.index(), e.name(), e.value())); + } + return this.staticIndex(e.index()); + } + + private BinaryRepresentationWriter configureDynamic(TableEntry e, long base) { + assert !e.isStaticTable(); + // RFC-9204: 3.2.6. Post-Base Indexing + // Post-Base indices are used in field line representations for entries with absolute + // indices greater than or equal to Base, starting at 0 for the entry with absolute index + // equal to Base and increasing in the same direction as the absolute index. + boolean usePostBase = e.index() >= base; + long index = usePostBase ? e.index() - base : base - 1 - e.index(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Indexed Field Line Dynamic Table reference %s (%s[%s], '%s', '%s')", + usePostBase ? "with Post-Base Index" : "", index, e.index(), e.name(), e.value())); + } + if (usePostBase) { + return dynamicPostBaseIndex(index); + } else { + return dynamicIndex(index); + } + } + + @Override + public boolean write(ByteBuffer destination) { + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } + + private FieldLineIndexedWriter staticIndex(long absoluteIndex) { + int N = 6; + intWriter.configure(absoluteIndex, N, 0b1100_0000); + return this; + } + + private FieldLineIndexedWriter dynamicIndex(long relativeIndex) { + assert relativeIndex >= 0; + int N = 6; + intWriter.configure(relativeIndex, N, 0b1000_0000); + return this; + } + + private FieldLineIndexedWriter dynamicPostBaseIndex(long relativeIndex) { + assert relativeIndex >= 0; + int N = 4; + intWriter.configure(relativeIndex, N, 0b0001_0000); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java new file mode 100644 index 00000000000..42187c256d7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +class FieldLineLiteralsWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + public FieldLineLiteralsWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, boolean hideIntermediary) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With Name and Value Literals ('%s', '%s', huffmanName=%b, huffmanValue=%b, hideIntermediary=%b)", + e.name(), e.value(), e.huffmanName(), e.huffmanValue(), hideIntermediary)); + } + return this.name(e, hideIntermediary).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!nameWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + nameWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private FieldLineLiteralsWriter name(TableEntry e, boolean hideIntermediary) { + int N = 3; + int payload = 0b0010_0000; + if (hideIntermediary) { + payload |= 0b0001_0000; + } + if (e.huffmanName()) { + payload |= 0b0000_1000; + } + nameWriter.configure(e.name(), N, payload, e.huffmanName()); + return this; + } + + private FieldLineLiteralsWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java new file mode 100644 index 00000000000..18e1d1e2676 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.FieldSectionPrefix; + +import java.nio.ByteBuffer; + +public class FieldLineSectionPrefixWriter { + enum State {NEW, CONFIGURED, RIC_WRITTEN, DONE} + + private final IntegerWriter intWriter; + private State state = State.NEW; + private long encodedRic; + private int signBit; + private long deltaBase; + + public FieldLineSectionPrefixWriter() { + this.intWriter = new IntegerWriter(); + } + + private void encodeFieldSectionPrefixFields(FieldSectionPrefix fsp, long maxEntries) { + // Required Insert Count encoded according to RFC-9204 "4.5.1.1: Required Insert Count" + // Base and Sign encoded according to RFC-9204: "4.5.1.2. Base" + long ric = fsp.requiredInsertCount(); + long base = fsp.base(); + + if (ric == 0) { + encodedRic = 0; + deltaBase = 0; + signBit = 0; + } else { + encodedRic = (ric % (2 * maxEntries)) + 1; + signBit = base >= ric ? 0 : 1; + deltaBase = base >= ric ? base - ric : ric - base - 1; + } + } + + public int configure(FieldSectionPrefix sectionPrefix, long maxEntries) { + intWriter.reset(); + encodeFieldSectionPrefixFields(sectionPrefix, maxEntries); + intWriter.configure(encodedRic, 8, 0); + state = State.CONFIGURED; + return IntegerWriter.requiredBufferSize(8, encodedRic) + + IntegerWriter.requiredBufferSize(7, deltaBase); + } + + public boolean write(ByteBuffer destination) { + if (state == State.NEW) { + throw new IllegalStateException("Configure first"); + } + + if (state == State.CONFIGURED) { + if (!intWriter.write(destination)) { + return false; + } + // Required Insert Count part is written, + // prepare integer writer for delta base and + // base sign write + intWriter.reset(); + int signPayload = signBit == 1 ? 0b1000_0000 : 0b0000_0000; + intWriter.configure(deltaBase, 7, signPayload); + state = State.RIC_WRITTEN; + } + + if (state == State.RIC_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = State.DONE; + } + return state == State.DONE; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java new file mode 100644 index 00000000000..3841ca7c5ff --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class HeaderFrameWriter { + private BinaryRepresentationWriter writer; + private final QPACK.Logger logger; + private final FieldLineIndexedWriter indexedWriter; + private final FieldLineIndexedNameWriter literalWithNameReferenceWriter; + private final FieldLineLiteralsWriter literalWithLiteralNameWriter; + private boolean encoding; + private static final AtomicLong HEADER_FRAME_WRITER_IDS = new AtomicLong(); + + public HeaderFrameWriter() { + this(QPACK.getLogger()); + } + + public HeaderFrameWriter(QPACK.Logger parentLogger) { + long id = HEADER_FRAME_WRITER_IDS.incrementAndGet(); + this.logger = parentLogger.subLogger("HeaderFrameWriter#" + id); + + indexedWriter = new FieldLineIndexedWriter(logger.subLogger("FieldLineIndexedWriter")); + literalWithNameReferenceWriter = new FieldLineIndexedNameWriter( + logger.subLogger("FieldLineIndexedNameWriter")); + literalWithLiteralNameWriter = new FieldLineLiteralsWriter( + logger.subLogger("FieldLineLiteralsWriter")); + } + + public void configure(TableEntry e, boolean sensitive, long base) { + checkIfEncodingInProgress(); + encoding = true; + writer = switch (e.type()) { + case NAME_VALUE -> indexedWriter.configure(e, base); + case NAME -> literalWithNameReferenceWriter.configure(e, sensitive, base); + case NEITHER -> literalWithLiteralNameWriter.configure(e, sensitive); + }; + } + + /** + * Writes the {@linkplain #configure(TableEntry, boolean, long) + * set up} header into the given buffer. + * + *

    The method writes as much as possible of the header's binary + * representation into the given buffer, starting at the buffer's position, + * and increments its position to reflect the bytes written. The buffer's + * mark and limit will not be modified. + * + *

    Once the method has returned {@code true}, the configured header is + * deemed encoded. A new header may be set up. + * + * @param headerFrame the buffer to encode the header into, may be empty + * @return {@code true} if the current header has been fully encoded, + * {@code false} otherwise + * @throws NullPointerException if the buffer is {@code null} + * @throws IllegalStateException if there is no set up header + */ + public boolean write(ByteBuffer headerFrame) { + if (!encoding) { + throw new IllegalStateException("A header hasn't been set up"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", headerFrame)); + } + boolean done = writer.write(headerFrame); + if (done) { + writer.reset(); + encoding = false; + } + return done; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException("Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java new file mode 100644 index 00000000000..f042135b4a1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public final class IntegerWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_WRITTEN = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int payload; + private int N; + private long value; + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | | | | | | | | | + // +---+---+---+-------------------+ + // |<--------->|<----------------->| + // payload N=5 + // + // payload is the contents of the left-hand side part of the first octet; + // it is truncated to fit into 8-N bits, where 1 <= N <= 8; + // + public IntegerWriter configure(long value, int N, int payload) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + if (value < 0) { + throw new IllegalArgumentException("value >= 0: value=" + value); + } + checkPrefix(N); + this.value = value; + this.N = N; + this.payload = payload & 0xFF & (0xFFFFFFFF << N); + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + if (value < max) { + output.put((byte) (payload | value)); + state = DONE; + return true; + } + output.put((byte) (payload | max)); + value -= max; + state = FIRST_BYTE_WRITTEN; + } + if (state == FIRST_BYTE_WRITTEN) { + while (value >= 128 && output.hasRemaining()) { + output.put((byte) ((value & 127) + 128)); + value /= 128; + } + if (!output.hasRemaining()) { + return false; + } + output.put((byte) value); + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, payload, N, value})); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public static int requiredBufferSize(int N, long value) { + checkPrefix(N); + int size = 1; + int max = (2 << (N - 1)) - 1; + if (value < max) { + return size; + } + size++; + value -= max; + while (value >= 128) { + value /= 128; + size++; + } + return size; + } + + public IntegerWriter reset() { + state = NEW; + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java new file mode 100644 index 00000000000..298c3d8f9c1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.hpack.ISO_8859_1; +import jdk.internal.net.http.hpack.Huffman; +import jdk.internal.net.http.hpack.QuickHuffman; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +// StringWriter does not require a notion of endOfInput (isLast) in 'write' +// methods due to the nature of string representation in HPACK. Namely, the +// length of the string is put before string's contents. Therefore the length is +// always known beforehand. +// +// Expected use: +// +// configure write* (reset configure write*)* +// +public final class StringWriter { + private static final int DEFAULT_PREFIX = 7; + private static final int DEFAULT_PAYLOAD = 0b0000_0000; + private static final int HUFFMAN_PAYLOAD = 0b1000_0000; + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int LENGTH_WRITTEN = 2; + private static final int DONE = 4; + + private final IntegerWriter intWriter = new IntegerWriter(); + private final Huffman.Writer huffmanWriter = new QuickHuffman.Writer(); + private final ISO_8859_1.Writer plainWriter = new ISO_8859_1.Writer(); + + private int state = NEW; + private boolean huffman; + + public StringWriter configure(CharSequence input, boolean huffman) { + return configure(input, 0, input.length(), DEFAULT_PREFIX, huffman ? HUFFMAN_PAYLOAD : DEFAULT_PAYLOAD, huffman); + } + + public StringWriter configure(CharSequence input, int N, int payload, boolean huffman) { + return configure(input, 0, input.length(), N, payload, huffman); + } + + StringWriter configure(CharSequence input, + int start, + int end, + int N, + int payload, + boolean huffman) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + if (!huffman) { + plainWriter.configure(input, start, end); + intWriter.configure(end - start, N, payload); + } else { + huffmanWriter.from(input, start, end); + intWriter.configure(huffmanWriter.lengthOf(input, start, end), N, payload); + } + + this.huffman = huffman; + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == DONE) { + return true; + } + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + if (intWriter.write(output)) { + state = LENGTH_WRITTEN; + } else { + return false; + } + } + if (state == LENGTH_WRITTEN) { + boolean written = huffman + ? huffmanWriter.write(output) + : plainWriter.write(output); + if (written) { + state = DONE; + return true; + } else { + return false; + } + } + throw new InternalError(Arrays.toString(new Object[]{state, huffman})); + } + + public void reset() { + intWriter.reset(); + if (huffman) { + huffmanWriter.reset(); + } else { + plainWriter.reset(); + } + state = NEW; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java new file mode 100644 index 00000000000..9f2ebf17264 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java @@ -0,0 +1,707 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A class that allows to read data from an aggregation of {@code ByteBuffer}. + * This is mostly geared to reading Quic or HTTP/3 frames that are composed + * of an aggregation of {@linkplain VariableLengthEncoder Variable Length Integers}. + * This class is not multi-thread safe. + *

    + * The {@code BuffersReader} class is an abstract class with two concrete + * implementations: {@link SingleBufferReader} and {@link ListBuffersReader}. + *

    + * The {@link SingleBufferReader} presents a simple lightweight view of a single + * {@link ByteBuffer}. Instances of {@code SingleBufferReader} can be created by + * calling {@link BuffersReader#single(ByteBuffer) BuffersReader.single(buffer)}; + *

    + * The {@link ListBuffersReader} view can be created from a (possibly empty) + * list of byte buffers. New byte buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} to the {@link ListBuffersReader} instance + * as they become available. Once a frame has been fully received, + * {@link BuffersReader#release()} or {@link BuffersReader#getAndRelease(long)} should + * be called to forget and relinquish all bytes buffers up to the current + * {@linkplain #position() position} of the {@code BuffersReader}. + * Released buffers are removed from {@code BuffersReader} list, and the position + * of the reader is reset to 0, allowing to read the next frame from the remaining + * data. + */ +public abstract sealed class BuffersReader { + + /** + * Release all buffers held by this {@code BuffersReader}, whether + * consumed or unconsumed. Released buffer are all set to their + * limit. + */ + public abstract void clear(); + + // Used to store the original position and limit of a + // buffer at the time it's added to the reader's list + // It is not possible to beyond that position or limit when + // using the reader + private record Buffer(ByteBuffer buffer, int offset, int limit) { + Buffer { + assert offset <= limit; + assert offset >= 0; + assert limit == buffer.limit(); + } + Buffer(ByteBuffer buffer) { + this(buffer, buffer.position(), buffer.limit()); + } + } + + /** + * {@return the current position of the reader} + * The semantic is similar to {@link ByteBuffer#position()}. + */ + public abstract long position(); + + /** + * {@return the limit of the reader} + * The semantic is similar to {@link ByteBuffer#limit()}. + */ + public abstract long limit(); + + /** + * Reads one byte from the reader. This method increase + * the position by one. The semantic is similar to + * {@link ByteBuffer#get()}. + * @return the byte at the current position + * @throws BufferUnderflowException if trying to read past + * the limit. + */ + public abstract byte get(); + + /** + * Reads the byte located at the given position in the + * reader. The semantic is similar to {@link ByteBuffer#get(int)}. + * This method doesn't change the position of the reader. + * + * @param position the position of the byte + * @return the byte at the given position in the reader + * + * @throws IndexOutOfBoundsException if trying to read before + * the reader's position or after the reader's limit + */ + public abstract byte get(long position); + + /** + * Sets the position of the reader. + * The semantic is similar to {@link ByteBuffer#position(int)}. + * + * @param newPosition the new position + * + * @throws IllegalArgumentException if trying to set + * the position to a negative value, or to a value + * past the limit + */ + public abstract void position(long newPosition); + + /** + * Releases all the data that has been read, sets the + * reader's position to 0 and its limit to the amount + * of data remaining. + */ + public abstract void release(); + + /** + * Returns a list of {@code ByteBuffer} containing the + * requested amount of bytes, starting at the current + * position, then release all the data up to the new + * position, and reset the reader's position to 0 and + * the reader's limit to the amount of remaining data. + * . + * @param bytes the amount of bytes to read and move + * to the returned list. + * + * @return a list of {@code ByteBuffer} containing the next + * {@code bytes} of data, starting at the current position. + * + * @throws BufferUnderflowException if attempting to read past + * the limit + */ + public abstract List getAndRelease(long bytes); + + /** + * {@return true if the reader has remaining bytes to the read} + * The semantic is similar to {@link ByteBuffer#hasRemaining()}. + */ + public boolean hasRemaining() { + return position() < limit(); + } + + /** + * {@return the number of bytes that remain to read} + * The semantic is similar to {@link ByteBuffer#remaining()}. + */ + public long remaining() { + long rem = limit() - position(); + return rem > 0 ? rem : 0; + } + + /** + * {@return the cumulated amount of data that has been read in this + * {@code BuffersReader} since its creation} + * This number is not reset when calling {@link #release()}. + */ + public abstract long read(); + + /** + * {@return The offset of this {@code BuffersReader}} + * This is the position in the first {@code ByteBuffer} that + * was set on the reader. The {@code BuffersReader} will not + * allow to get or set a position lower than the offset. + */ + public abstract long offset(); + + /** + * {@return true if this {@code BuffersReader} is empty} + * A {@code BuffersReader} is empty if it has been {@linkplain + * #list() created empty, or if it has been {@linkplain #release() + * released} after all data has been read. + */ + public abstract boolean isEmpty(); + + /** + * A lightweight view allowing to see a {@link ByteBuffer} as a + * {@link BuffersReader}. This class wrap a single {@link ByteBuffer} + * and cannot be reused after {@link #release()}. + */ + public static final class SingleBufferReader extends BuffersReader { + ByteBuffer single; + long read = 0; + long start; + SingleBufferReader(ByteBuffer single) { + this.single = single; + start = single.position(); + } + + @Override + public void release() { + single = null; + } + + @Override + public List getAndRelease(long bytes) { + return List.of(getAndReleaseBuffer(bytes)); + } + + @Override + public byte get() { + if (single == null) throw new BufferUnderflowException(); + return single.get(); + } + + @Override + public byte get(long position) { + if (single == null || position < start || position >= single.limit()) + throw new IndexOutOfBoundsException(); + return single.get((int) position); + } + + @Override + public long limit() { + return single == null ? 0 : single.limit(); + } + + @Override + public long position() { + return single == null ? 0 : single.position(); + } + + @Override + public boolean hasRemaining() { + return single != null && single.hasRemaining(); + } + + @Override + public void position(long pos) { + if (single == null || pos < start || pos > single.limit()) + throw new BufferUnderflowException(); + single.position((int) pos); + } + + /** + * This method has the same semantics than {@link #getAndRelease(long)} + * except that it avoids creating a list. + * @return a buffer containing the next {@code bytes}. + */ + public ByteBuffer getAndReleaseBuffer(long bytes) { + var released = single; + int remaining = released.remaining(); + if (bytes > remaining) + throw new BufferUnderflowException(); + if (bytes == remaining) { + read = single.limit() - start; + single = null; + } else { + read = single.position() - start; + single = released.slice(released.position() + (int)bytes, released.limit()); + start = 0; + released = released.slice(released.position(), (int) bytes); + } + return released; + } + + @Override + public long read() { + return single == null ? read : (read + single.position() - start); + } + + @Override + public long offset() { + return start; + } + + @Override + public boolean isEmpty() { + return single == null; + } + + @Override + public void clear() { + if (single == null) return; + single.position(single.limit()); + single = null; + } + } + + /** + * A {@code BuffersReader} that iterates over a list of {@code ByteBuffers}. + * New {@code ByteBuffers} can be added at the end of list by calling + * {@link #add(ByteBuffer)} or {@link #addAll(List)}, which increases + * the {@linkplain #limit() limit} accordingly. + *

    + * When {@link #release() released}, the data prior to the current + * {@linkplain #position()} is discarded, the {@linkplain #position() position} + * and {@linkplain #offset() offset} are reset to {@code 0}, and the + * {@linkplain #limit() limit} is set to the amount of remaining data. + *

    + * A {@code ListBuffersReader} can be reused after being released. + * If it still contains data, the {@linkplain #offset() offset} will + * be {@code 0}. Otherwise, the offset will be set to the position + * of the first buffer {@linkplain #add(ByteBuffer) added} to the + * {@code ListBuffersReader}. + */ + public static final class ListBuffersReader extends BuffersReader { + private final List buffers = new ArrayList<>(); + private Buffer current; + private int nextIndex; + private long currentOffset; + private long position; + private long limit; + private long start; + private long readAndReleased = 0; + + ListBuffersReader() { + } + + /** + * Adds a new {@code ByteBuffer} to this {@code BuffersReader}. + * If the reader is {@linkplain #isEmpty() empty}, the reader's + * {@linkplain #offset() offset} and {@linkplain #position() position} + * is set to the buffer's position, and the reader {@linkplain #limit() + * limit} is set to the buffer's limit. + * Otherwise, the reader's limit is simply increased by the buffer's + * remaining bytes. The reader will only allow to read those bytes + * between the current position and limit of the buffer. + * + * @apiNote + * This class doesn't make defensive copies of the provided buffers, + * so the caller must not modify the buffer's position or limit + * after it's been added to the reader. + * + * @param buffer a byte buffer + * @return this reader + */ + public ListBuffersReader add(ByteBuffer buffer) { + if (buffers.isEmpty()) { + int lim = buffer.limit(); + buffers.add(new Buffer(buffer, 0, lim)); + start = buffer.position(); + position = limit = start; + currentOffset = 0; + } else { + buffers.add(new Buffer(buffer)); + } + limit += buffer.remaining(); + return this; + } + + /** + * Adds a list of byte buffers to this reader. + * This is equivalent to calling: + * {@snippet : + * ListBuffersReader reader = ...; + * for (var buffer : buffers) { + * reader.add(buffer); // @link substring="add" target="#add(ByteBuffer)" + * } + * } + * @param buffers a list of {@link ByteBuffer ByteBuffers} + * @return this reader + */ + public ListBuffersReader addAll(List buffers) { + for (var buffer : buffers) { + if (isEmpty()) { + add(buffer); + continue; + } + this.buffers.add(new Buffer(buffer)); + limit += buffer.remaining(); + } + return this; + } + + @Override + public boolean isEmpty() { + return buffers.isEmpty(); + } + + @Override + public byte get() { + ByteBuffer buffer = current(true); + byte res = buffer.get(); + position++; + return res; + } + + @Override + public byte get(long pos) { + if (pos >= limit || pos < start) + throw new IndexOutOfBoundsException(); + ByteBuffer buffer = current(false); + if (position == limit && current != null) { + // let the current buffer throw + buffer = current.buffer; + } + assert buffer != null : "limit check failed"; + if (pos == position) { + return buffer.get(buffer.position()); + } + long offset = currentOffset; + int index = nextIndex; + Buffer cur = current; + while (pos >= offset) { + int bpos = buffer.position(); + int boffset = cur.offset; + int blimit = buffer.limit(); + assert index == nextIndex || bpos == boffset; + if (pos - offset < blimit - boffset) { + return buffer.get((int) (pos - offset + boffset)); + } + if (index >= buffers.size()) { + assert false : "buffers exhausted"; + throw new IndexOutOfBoundsException(); + } + int skipped = cur.limit - cur.offset; + offset += skipped; + cur = buffers.get(index++); + buffer = cur.buffer; + } + assert pos <= offset; + int blimit = cur.offset; + int boffset = cur.offset; + while (pos < offset) { + assert blimit == cur.limit || index == nextIndex && blimit == boffset; + if (index <= 1) { + assert false : "buffers exhausted"; + throw new IndexOutOfBoundsException(); + } + cur = buffers.get(--index - 1); + buffer = cur.buffer; + int bpos = buffer.position(); + blimit = buffer.limit(); + boffset = cur.offset; + int skipped = blimit - boffset; + offset -= skipped; + assert index == nextIndex || bpos == blimit; + if (pos - offset >= 0 && pos - offset < blimit - boffset) { + return buffer.get((int) (pos - offset + boffset)); + } + } + assert false : "buffer not found"; + throw new IndexOutOfBoundsException(); // should not reach here + } + + /** + * {@return the current {@code ByteBuffer} in which to find + * the byte at the current {@link #position()}} + * + * @param throwIfUnderflow if true, calling this method + * will throw {@link BufferUnderflowException} if + * the position is past the limit. + * + * @throws BufferUnderflowException if attempting to read past + * the limit and {@code throwIfUnderflow == true} + */ + private ByteBuffer current(boolean throwIfUnderflow) { + while (current == null || !current.buffer.hasRemaining()) { + if (buffers.size() > nextIndex) { + if (nextIndex != 0) { + currentOffset = position; + } else { + currentOffset = 0; + } + current = buffers.get(nextIndex++); + } else if (throwIfUnderflow) { + throw new BufferUnderflowException(); + } else { + return null; + } + } + return current.buffer; + } + + @Override + public List getAndRelease(long bytes) { + release(); + if (bytes > limit - position) { + throw new BufferUnderflowException(); + } + ByteBuffer buf = current(false); + if (buf == null || bytes == 0) return List.of(); + List list = null; + assert position == 0; + assert currentOffset == 0; + while (bytes > 0) { + buf = current(false); + assert nextIndex == 1; + assert buf != null; + assert buf.position() == current.offset; + int remaining = buf.remaining(); + if (remaining <= bytes) { + var b = buffers.remove(--nextIndex); + assert b == current; + long relased = buf.remaining(); + assert b.buffer.limit() == b.limit; + bytes -= relased; + limit -= relased; + readAndReleased += relased; + current = null; + + // if a buffer has no remaining bytes it + // may be EOF. Let's not skip it here + // if (!buf.hasRemaining()) continue; + + if (bytes == 0 && list == null) { + list = List.of(buf); + } else { + if (list == null) { + list = new ArrayList<>(); + } + list.add(buf); + } + } else { + var b = current; + long relased = bytes; + bytes = 0; + limit -= relased; + var pos = buf.position(); + assert b.limit == buf.limit(); + assert pos == b.offset; + var slice = buf.slice(pos, (int)relased); + buf.position(pos + (int) relased); + buffers.set(nextIndex - 1, current = new Buffer(buf)); + readAndReleased += relased; + if (list != null) { + list.add(slice); + } else { + list = List.of(slice); + } + assert bytes == 0; + } + } + return list; + } + + @Override + public long position() { + return position; + } + + @Override + public long limit() { + return limit; + } + + @Override + public void release() { + long released = - start; + for (var it = buffers.listIterator(); it.hasNext(); ) { + var b = it.next(); + var buf = b.buffer; + released += (buf.position() - b.offset); + if (buf.hasRemaining()) { + it.set(new Buffer(buf)); + break; + } + it.remove(); + } + assert released == position - start + : "start=%s, position=%s, released=%s" + .formatted(start, position, released); + readAndReleased += released; + limit -= position; + current = null; + position = 0; + currentOffset = 0; + nextIndex = 0; + start = 0; + } + + @Override + public void position(long pos) { + if (pos > limit) throw new IllegalArgumentException(pos + " > " + limit); + if (pos < start) throw new IllegalArgumentException(pos + " < " + start); + if (pos == position) return; // happy case! + // look forward, starting from the current position: + // - identify the ByteBuffer that contains the requested position + // - set the local position in that ByteBuffer to + // match the requested position + if (pos > position) { + long skip = pos - position; + assert skip > 0; + while (skip > 0) { + var buffer = current(true); + int remaining = buffer.remaining(); + if (remaining == 0) continue; + if (skip > remaining) { + // somewhere after the current buffer + buffer.position(buffer.limit()); + position += remaining; + skip -= remaining; + } else { + // somewhere in the current buffer + buffer.position(buffer.position() + (int) skip); + position += skip; + skip = 0; + } + } + } else { + // look backward, starting from the current position: + // - identify the ByteBuffer that contains the requested position + // - set the local position in that ByteBuffer to + // match the requested position + long skip = pos - position; + assert skip < 0; + if (current == null) { + current(false); + if (current == null) + throw new IllegalArgumentException(); + } + while (skip < 0) { + var buffer = current.buffer; + assert buffer.limit() == current.limit; + var remaining = buffer.position() - current.offset; + var rest = skip + remaining; + if (rest >= 0) { + // somewhere in this byte buffer, between the + // buffer offset and the buffer position + buffer.position(buffer.position() + (int)skip); + position += skip; + assert position >= start; + skip = 0; + } else { + // in some buffer prior to the current byte buffer + buffer.position(current.offset); + skip += remaining; + position -= remaining; + assert skip < 0; + assert position >= start; + assert nextIndex > 1; + current = buffers.get(--nextIndex - 1); + currentOffset -= current.limit - current.offset; + assert currentOffset >= 0; + assert current.buffer.position() == current.limit; + } + } + } + } + + @Override + public long read() { + return readAndReleased + (position - start); + } + + @Override + public long offset() { + return start; + } + + @Override + public void clear() { + release(); + position(limit()); + release(); + } + } + + /** + * Creates a lightweight {@link SingleBufferReader} view over + * a single {@link ByteBuffer}. + * @param buffer a byte buffer + * @return a lightweight {@link SingleBufferReader} view over + * a single {@link ByteBuffer} + */ + public static SingleBufferReader single(ByteBuffer buffer) { + return new SingleBufferReader(Objects.requireNonNull(buffer)); + } + + /** + * Creates an {@linkplain #isEmpty() empty} {@link ListBuffersReader}. + * @return an empty {@code ListBuffersReader} + */ + public static ListBuffersReader list() { + return new ListBuffersReader(); + } + + /** + * Creates a {@link ListBuffersReader} with the given + * {@code buffer}. More buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} as they become + * available. + * @return a {@code ListBuffersReader} + */ + public static ListBuffersReader list(ByteBuffer buffer) { + return new ListBuffersReader().add(buffer); + } + + /** + * Creates a {@link ListBuffersReader} with the given + * {@code buffers} list. More buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} as they become + * available. + * @return a {@code ListBuffersReader} + */ + public static ListBuffersReader list(List buffers) { + return new ListBuffersReader().addAll(buffers); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java new file mode 100644 index 00000000000..d2daa6fadaf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public interface CodingContext { + + /** + * {@return the largest incoming packet number successfully processed + * in the given packet number space} + * + * @apiNote + * This method is used when decoding the packet number of an incoming packet. + * + * @param packetSpace the packet number space + */ + long largestProcessedPN(QuicPacket.PacketNumberSpace packetSpace); + + /** + * {@return the largest outgoing packet number acknowledged by the peer + * in the given packet number space} + * + * @apiNote + * This method is used when encoding the packet number of an outgoing packet. + * + * @param packetSpace the packet number space + */ + long largestAckedPN(QuicPacket.PacketNumberSpace packetSpace); + + /** + * {@return the length of the local connection ids expected + * to be found in incoming short header packets} + */ + int connectionIdLength(); + + /** + * {@return the largest incoming packet number successfully processed + * in the packet number space corresponding to the given packet type} + *

    + * This is equivalent to calling:

    +     *     {@code largestProcessedPN(QuicPacket.PacketNumberSpace.of(packetType));}
    +     * 
    + * + * @apiNote + * This method is used when decoding the packet number of an incoming packet. + * + * @param packetType the packet type + */ + default long largestProcessedPN(QuicPacket.PacketType packetType) { + return largestProcessedPN(QuicPacket.PacketNumberSpace.of(packetType)); + } + + /** + * {@return the largest outgoing packet number acknowledged by the peer + * in the packet number space corresponding to the given packet type} + *

    + * This is equivalent to calling:

    +     *     {@code largestAckedPN(QuicPacket.PacketNumberSpace.of(packetType));}
    +     * 
    + * + * @apiNote + * This method is used when encoding the packet number of an outgoing packet. + * + * @param packetType the packet type + */ + default long largestAckedPN(QuicPacket.PacketType packetType) { + return largestAckedPN(QuicPacket.PacketNumberSpace.of(packetType)); + } + + /** + * Writes the given outgoing packet in the given byte buffer. + * This method moves the position of the byte buffer. + * @param packet the outgoing packet to write + * @param buffer the byte buffer to write the packet into + * @return the number of bytes written + * @throws java.nio.BufferOverflowException if the buffer doesn't have + * enough space to write the packet + */ + int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Reads an encrypted packet from the given byte buffer. + * This method moves the position of the byte buffer. + * @param src a byte buffer containing a non encrypted packet + * @return the packet read + * @throws IOException if the packet couldn't be read + * @throws QuicTransportException if packet is correctly signed but malformed + */ + QuicPacket parsePacket(ByteBuffer src) throws IOException, QuicKeyUnavailableException, QuicTransportException; + + /** + * Returns the original destination connection id, required for + * calculating the retry integrity tag. + *

    + * This is only of interest when protecting/unprotecting a {@linkplain + * QuicPacket.PacketType#RETRY Retry Packet}. + * + * @return the original destination connection id, required for calculating + * the retry integrity tag + */ + QuicConnectionId originalServerConnId(); + + /** + * Returns the TLS engine associated with this context + * @return the TLS engine associated with this context + */ + QuicTLSEngine getTLSEngine(); + + /** + * Checks if the provided token is valid for the given context and connection ID. + * @param destinationID destination connection ID found in the packet + * @param token token to verify + * @return true if token is valid, false otherwise + */ + boolean verifyToken(QuicConnectionId destinationID, byte[] token); + + /** + * {@return The minimum payload size for short packet payloads}. + * Padding will be added to match that size if needed. + * @param destConnectionIdLength the length of the destination + * connectionId included in the packet + */ + default int minShortPacketPayloadSize(int destConnectionIdLength) { + // See RFC 9000, Section 10.3 + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3 + // [..] the endpoint SHOULD ensure that all packets it sends + // are at least 22 bytes longer than the minimum connection + // ID length that it requests the peer to include in its + // packets [...] + // + // A 1-RTT packet contains the peer connection id + // (whose length is destConnectionIdLength), therefore the + // payload should be at least 5 - (destConnectionIdLength + // - connectionIdLength()) - where connectionIdLength is the + // length of the local connection ID. + return 5 - (destConnectionIdLength - connectionIdLength()); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java new file mode 100644 index 00000000000..24230a0883d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +// responsible for managing the connection termination of a QUIC connection +public sealed interface ConnectionTerminator permits ConnectionTerminatorImpl { + + // lets the terminator know that the connection is still alive and should not be + // idle timed out + void keepAlive(); + + void terminate(TerminationCause cause); + + boolean tryReserveForUse(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java new file mode 100644 index 00000000000..150d6233953 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicConnectionImpl.HandshakeFlow; +import jdk.internal.net.http.quic.QuicConnectionImpl.ProtectionRecord; +import jdk.internal.net.http.quic.TerminationCause.AppLayerClose; +import jdk.internal.net.http.quic.TerminationCause.SilentTermination; +import jdk.internal.net.http.quic.TerminationCause.TransportError; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.CLOSED; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.CLOSING; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.DRAINING; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.INTERNAL_ERROR; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +final class ConnectionTerminatorImpl implements ConnectionTerminator { + + private final QuicConnectionImpl connection; + private final Logger debug; + private final String logTag; + private final AtomicReference terminationCause = new AtomicReference<>(); + private final CompletableFuture futureTC = new MinimalFuture<>(); + + ConnectionTerminatorImpl(final QuicConnectionImpl connection) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.debug = connection.debug; + this.logTag = connection.logTag(); + } + + @Override + public void keepAlive() { + this.connection.idleTimeoutManager.keepAlive(); + } + + @Override + public boolean tryReserveForUse() { + return this.connection.idleTimeoutManager.tryReserveForUse(); + } + + @Override + public void terminate(final TerminationCause cause) { + Objects.requireNonNull(cause); + try { + doTerminate(cause); + } catch (Throwable t) { + // make sure we do fail the handshake CompletableFuture(s) + // even when the connection termination itself failed. that way + // the dependent CompletableFuture(s) tasks don't keep waiting forever + failHandshakeCFs(t); + } + } + + TerminationCause getTerminationCause() { + return this.terminationCause.get(); + } + + private void doTerminate(final TerminationCause cause) { + final ConnectionCloseFrame frame; + KeySpace keySpace; + switch (cause) { + case SilentTermination st -> { + silentTerminate(st); + return; + } + case TransportError te -> { + frame = new ConnectionCloseFrame(te.getCloseCode(), te.frameType, + te.getPeerVisibleReason()); // 0x1c + keySpace = te.keySpace; + } + case TerminationCause.InternalError ie -> { + frame = new ConnectionCloseFrame(ie.getCloseCode(), 0, + ie.getPeerVisibleReason()); // 0x1c + keySpace = null; + } + case AppLayerClose alc -> { + // application layer triggered connection close + frame = new ConnectionCloseFrame(alc.getCloseCode(), + alc.getPeerVisibleReason()); // 0x1d + keySpace = null; + } + } + if (keySpace == null) { + // TODO: review this + keySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + } + immediateClose(frame, keySpace, cause); + } + + void incomingConnectionCloseFrame(final ConnectionCloseFrame frame) { + Objects.requireNonNull(frame); + if (debug.on()) { + debug.log("Received close frame: %s", frame); + } + drain(frame); + } + + void incomingStatelessReset() { + // if local endpoint is a client, then our peer is a server + final boolean peerIsServer = connection.isClientConnection(); + if (Log.errors()) { + Log.logError("{0}: stateless reset from peer ({1})", connection.logTag(), + (peerIsServer ? "server" : "client")); + } + final SilentTermination st = forSilentTermination("stateless reset from peer (" + + (peerIsServer ? "server" : "client") + ")"); + terminate(st); + } + + /** + * Called only when the connection is expected to be discarded without being required + * to inform the peer. + * Discards all state, no CONNECTION_CLOSE is sent, nor does the connection enter closing + * or discarding state. + */ + private void silentTerminate(final SilentTermination terminationCause) { + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for this connection + connection.idleTimeoutManager.shutdown(); + // mark the connection state as closed (we don't enter closing or draining state + // during silent termination) + if (!markClosed(terminationCause)) { + // previously already closed + return; + } + if (Log.quic()) { + Log.logQuic("{0} silently terminating connection due to: {1}", + logTag, terminationCause.getLogMsg()); + } else if (debug.on()) { + debug.log("silently terminating connection due to: " + terminationCause.getLogMsg()); + } + if (debug.on() || Log.quic()) { + String message = connection.loggableState(); + if (message != null) { + Log.logQuic("{0} connection state: {1}", logTag, message); + debug.log("connection state: %s", message); + } + } + failHandshakeCFs(); + // remove from the endpoint + unregisterConnFromEndpoint(); + discardConnectionState(); + // terminate the streams + connection.streams.terminate(terminationCause); + } + + CompletableFuture futureTerminationCause() { + return this.futureTC; + } + + private void unregisterConnFromEndpoint() { + final QuicEndpoint endpoint = this.connection.endpoint(); + if (endpoint == null) { + // this can happen if the connection is being terminated before + // an endpoint has been established (which is OK) + return; + } + endpoint.removeConnection(this.connection); + } + + private void immediateClose(final ConnectionCloseFrame closeFrame, + final KeySpace keySpace, + final TerminationCause terminationCause) { + assert closeFrame != null : "connection close frame is null"; + assert keySpace != null : "keyspace is null"; + final String logMsg = terminationCause.getLogMsg(); + // if the connection has already been closed (for example: through silent termination) + // then the local state of the connection is already discarded and thus + // there's nothing more we can do with the connection. + if (connection.stateHandle().isMarked(CLOSED)) { + return; + } + // switch to closing state + if (!markClosing(terminationCause)) { + // has previously already gone into closing state + return; + } + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for a closing connection + connection.idleTimeoutManager.shutdown(); + + if (connection.stateHandle().draining()) { + if (Log.quic()) { + Log.logQuic("{0} skipping immediate close, since connection is already" + + " in draining state", logTag, logMsg); + } else if (debug.on()) { + debug.log("skipping immediate close, since connection is already" + + " in draining state"); + } + // we are already (in the subsequent) draining state, no need to anything more + return; + } + try { + final String closeCodeHex = (terminationCause.isAppLayer() ? "(app layer) " : "") + + "0x" + Long.toHexString(closeFrame.errorCode()); + if (Log.quic()) { + Log.logQuic("{0} entering closing state, code {1} - {2}", logTag, closeCodeHex, logMsg); + } else if (debug.on()) { + debug.log("entering closing state, code " + closeCodeHex + " - " + logMsg); + } + pushConnectionCloseFrame(keySpace, closeFrame); + } catch (Exception e) { + if (Log.errors()) { + Log.logError("{0} removing connection from endpoint after failure to send" + + " CLOSE_CONNECTION: {1}", logTag, e); + } else if (debug.on()) { + debug.log("removing connection from endpoint after failure to send" + + " CLOSE_CONNECTION"); + } + // we failed to send a CONNECTION_CLOSE frame. this implies that the QuicEndpoint + // won't detect that the QuicConnectionImpl has transitioned to closing connection + // and thus won't remap it to closing. we thus discard such connection from the + // endpoint. + unregisterConnFromEndpoint(); + } + failHandshakeCFs(); + discardConnectionState(); + connection.streams.terminate(terminationCause); + if (Log.quic()) { + Log.logQuic("{0} connection has now transitioned to closing state", logTag); + } else if (debug.on()) { + debug.log("connection has now transitioned to closing state"); + } + } + + private void drain(final ConnectionCloseFrame incomingFrame) { + // if the connection has already been closed (for example: through silent termination) + // then the local state of the connection is already discarded and thus + // there's nothing more we can do with the connection. + if (connection.stateHandle().isMarked(CLOSED)) { + return; + } + final boolean isAppLayerClose = incomingFrame.variant(); + final String closeCodeString = isAppLayerClose ? + "[app]" + connection.quicInstance().appErrorToString(incomingFrame.errorCode()) : + QuicTransportErrors.toString(incomingFrame.errorCode()); + final String reason = incomingFrame.reasonString(); + final String peer = connection.isClientConnection() ? "server" : "client"; + final String msg = "Connection closed by " + peer + " peer: " + + closeCodeString + + (reason == null || reason.isEmpty() ? "" : (" " + reason)); + final TerminationCause terminationCause; + if (isAppLayerClose) { + terminationCause = appLayerClose(incomingFrame.errorCode(), msg) + .peerVisibleReason(reason); + } else { + terminationCause = forTransportError(incomingFrame.errorCode(), msg, + incomingFrame.errorFrameType()) + .peerVisibleReason(reason); + } + // switch to draining state + if (!markDraining(terminationCause)) { + // has previously already gone into draining state + return; + } + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for a closing connection + connection.idleTimeoutManager.shutdown(); + + if (Log.quic()) { + Log.logQuic("{0} entering draining state, {1}", logTag, + terminationCause.getLogMsg()); + } else if (debug.on()) { + debug.log("entering draining state, " + + terminationCause.getLogMsg()); + } + // RFC-9000, section 10.2.2: + // An endpoint that receives a CONNECTION_CLOSE frame MAY send a single packet containing + // a CONNECTION_CLOSE frame before entering the draining state, using a NO_ERROR code if + // appropriate. An endpoint MUST NOT send further packets. + // if we had previously marked our state as closing, then that implies + // we would have already sent a connection close frame. we won't send + // another when draining in such a case. + if (markClosing(terminationCause)) { + try { + if (Log.quic()) { + Log.logQuic("{0} sending CONNECTION_CLOSE frame before entering draining state", + logTag); + } else if (debug.on()) { + debug.log("sending CONNECTION_CLOSE frame before entering draining state"); + } + final ConnectionCloseFrame outgoingFrame = + new ConnectionCloseFrame(NO_ERROR.code(), incomingFrame.getTypeField(), null); + final KeySpace currentKeySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + pushConnectionCloseFrame(currentKeySpace, outgoingFrame); + } catch (Exception e) { + // just log and ignore, since sending the CONNECTION_CLOSE when entering + // draining state is optional + if (Log.errors()) { + Log.logError(logTag + " Failed to send CONNECTION_CLOSE frame," + + " when entering draining state: {0}", e); + } else if (debug.on()) { + debug.log("failed to send CONNECTION_CLOSE frame, when entering" + + " draining state: " + e); + } + } + } + failHandshakeCFs(); + // remap the connection to a draining connection + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + endpoint.draining(connection); + discardConnectionState(); + connection.streams.terminate(terminationCause); + if (Log.quic()) { + Log.logQuic("{0} connection has now transitioned to draining state", logTag); + } else if (debug.on()) { + debug.log("connection has now transitioned to draining state"); + } + } + + private void discardConnectionState() { + // close packet spaces + connection.packetNumberSpaces().close(); + // close the incoming packets buffered queue + connection.closeIncoming(); + } + + private void failHandshakeCFs() { + final TerminationCause tc = this.terminationCause.get(); + assert tc != null : "termination cause is null"; + failHandshakeCFs(tc.getCloseCause()); + } + + private void failHandshakeCFs(final Throwable cause) { + final HandshakeFlow handshakeFlow = connection.handshakeFlow(); + handshakeFlow.failHandshakeCFs(cause); + } + + private boolean markClosing(final TerminationCause terminationCause) { + return mark(CLOSING, terminationCause); + } + + private boolean markDraining(final TerminationCause terminationCause) { + return mark(DRAINING, terminationCause); + } + + private boolean markClosed(final TerminationCause terminationCause) { + return mark(CLOSED, terminationCause); + } + + private boolean mark(final int mask, final TerminationCause cause) { + assert cause != null : "termination cause is null"; + final boolean causeSet = this.terminationCause.compareAndSet(null, cause); + // first mark the state appropriately, before completing the futureTerminationCause + // completable future, so that any dependent actions on the completable future + // will see the right state + final boolean marked = this.connection.stateHandle().mark(mask); + if (causeSet) { + this.futureTC.completeAsync(() -> cause, connection.quicInstance().executor()); + } + return marked; + } + + /** + * CONNECTION_CLOSE frame is not congestion controlled (RFC-9002 section 3 + * and RFC-9000 section 12.4, table 3), nor is it queued or scheduled for sending. + * This method constructs a {@link QuicPacket} containing the {@code frame} and immediately + * {@link QuicConnectionImpl#pushDatagram(ProtectionRecord) pushes the datagram} through + * the connection. + * + * @param keySpace the KeySpace to use for sending the packet + * @param frame the CONNECTION_CLOSE frame + * @throws QuicKeyUnavailableException if the keys for the KeySpace aren't available + * @throws QuicTransportException for any QUIC transport exception when sending the packet + */ + private void pushConnectionCloseFrame(final KeySpace keySpace, + final ConnectionCloseFrame frame) + throws QuicKeyUnavailableException, QuicTransportException { + // ConnectionClose frame is allowed in Initial, Handshake, 0-RTT, 1-RTT spaces. + // for Initial and Handshake space, the frame is expected to be of type 0x1c. + // see RFC-9000, section 12.4, Table 3 for additional details + final ConnectionCloseFrame toSend = switch (keySpace) { + case ONE_RTT, ZERO_RTT -> frame; + case INITIAL, HANDSHAKE -> { + // RFC 9000 - section 10.2.3: + // A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE + // of type 0x1c when sending the frame in Initial or Handshake packets. + // Otherwise, information about the application state might be revealed. + // Endpoints MUST clear the value of the Reason Phrase field and SHOULD + // use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE + // of type 0x1c. + yield frame.clearApplicationState(); + } + default -> { + throw new IllegalStateException("cannot send a connection close frame" + + " in keyspace: " + keySpace); + } + }; + final QuicPacket packet = connection.newQuicPacket(keySpace, List.of(toSend)); + final ProtectionRecord protectionRecord = ProtectionRecord.single(packet, + connection::allocateDatagramForEncryption); + // while sending the packet containing the CONNECTION_CLOSE frame, the pushDatagram will + // remap (or remove) the QuicConnectionImpl in QuicEndpoint. + connection.pushDatagram(protectionRecord); + } + + /** + * Returns a {@link ByteBuffer} which contains an encrypted QUIC packet containing + * a {@linkplain ConnectionCloseFrame CONNECTION_CLOSE frame}. The CONNECTION_CLOSE + * frame will have a frame type of {@code 0x1c} and error code of {@code NO_ERROR}. + *

    + * This method should only be invoked when the {@link QuicEndpoint} is being closed + * and the endpoint wants to send out a {@code CONNECTION_CLOSE} frame on a best-effort + * basis (in a fire and forget manner). + * + * @return the datagram containing the QUIC packet with a CONNECTION_CLOSE frame + * @throws QuicKeyUnavailableException + * @throws QuicTransportException + */ + ByteBuffer makeConnectionCloseDatagram() + throws QuicKeyUnavailableException, QuicTransportException { + // in theory we don't need this assert, but given the knowledge that this method + // should only be invoked by a closing QuicEndpoint, we have this assert here to + // prevent misuse of this makeConnectionCloseDatagram() method + assert connection.endpoint().isClosed() : "QUIC endpoint isn't closed"; + final ConnectionCloseFrame connCloseFrame = new ConnectionCloseFrame(NO_ERROR.code(), + QuicFrame.CONNECTION_CLOSE, null); + final KeySpace keySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + // we don't want the connection's ByteBuffer pooling infrastructure + // (through the QuicConnectionImpl::allocateDatagramForEncryption) for + // this packet, so we use a simple custom allocator. + final Function allocator = (pkt) -> ByteBuffer.allocate(pkt.size()); + final QuicPacket packet = connection.newQuicPacket(keySpace, List.of(connCloseFrame)); + final ProtectionRecord encrypted = ProtectionRecord.single(packet, allocator) + .encrypt(connection.codingContext()); + final ByteBuffer datagram = encrypted.datagram(); + final int firstPacketOffset = encrypted.firstPacketOffset(); + // flip the datagram + datagram.limit(datagram.position()); + datagram.position(firstPacketOffset); + return datagram; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java new file mode 100644 index 00000000000..a7469f18ed8 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.quic.QuicTLSEngine; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; + +/** + * Keeps track of activity on a {@code QuicConnectionImpl} and manages + * the idle timeout of the QUIC connection + */ +final class IdleTimeoutManager { + + private static final long NO_IDLE_TIMEOUT = 0; + + private final QuicConnectionImpl connection; + private final Logger debug; + private final AtomicBoolean shutdown = new AtomicBoolean(); + private final AtomicLong idleTimeoutDurationMs = new AtomicLong(); + private final ReentrantLock stateLock = new ReentrantLock(); + // must be accessed only when holding stateLock + private IdleTimeoutEvent idleTimeoutEvent; + // must be accessed only when holding stateLock + private StreamDataBlockedEvent streamDataBlockedEvent; + // the time at which the last outgoing packet was sent or an + // incoming packet processed on the connection + private volatile long lastPacketActivityAt; + + private final ReentrantLock idleTerminationLock = new ReentrantLock(); + // true if it has been decided to terminate the connection due to being idle, + // false otherwise. should be accessed only when holding the idleTerminationLock + private boolean chosenForIdleTermination; + // the time at which the connection was last reserved for use. + // should be accessed only when holding the idleTerminationLock + private long lastUsageReservationAt; + + IdleTimeoutManager(final QuicConnectionImpl connection) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.debug = connection.debug; + } + + /** + * Starts the idle timeout management for the connection. This should be called + * after the handshake is complete for the connection. + * + * @throw IllegalStateException if handshake hasn't yet completed or if the handshake + * has failed for the connection + */ + void start() { + final CompletableFuture handshakeCF = + this.connection.handshakeFlow().handshakeCF(); + // start idle management only for successfully completed handshake + if (!handshakeCF.isDone()) { + throw new IllegalStateException("handshake isn't yet complete," + + " cannot start idle connection management"); + } + if (handshakeCF.isCompletedExceptionally()) { + throw new IllegalStateException("cannot start idle connection management for a failed" + + " connection"); + } + startTimers(); + } + + /** + * Starts the idle timeout timer of the QUIC connection, if not already started. + */ + private void startTimers() { + if (shutdown.get()) { + return; + } + this.stateLock.lock(); + try { + if (shutdown.get()) { + return; + } + startIdleTerminationTimer(); + startStreamDataBlockedTimer(); + } finally { + this.stateLock.unlock(); + } + } + + private void startIdleTerminationTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + final Optional idleTimeoutMillis = getIdleTimeout(); + if (idleTimeoutMillis.isEmpty()) { + if (debug.on()) { + debug.log("idle connection management disabled for connection"); + } else { + Log.logQuic("{0} idle connection management disabled for connection", + connection.logTag()); + } + return; + } + final QuicTimerQueue timerQueue = connection.endpoint().timer(); + final Deadline deadline = timeLine().instant().plusMillis(idleTimeoutMillis.get()); + // we don't expect idle timeout management to be started more than once + assert this.idleTimeoutEvent == null : "idle timeout management" + + " already started for connection"; + // create the idle timeout event and register with the QuicTimerQueue. + this.idleTimeoutEvent = new IdleTimeoutEvent(deadline); + timerQueue.offer(this.idleTimeoutEvent); + if (debug.on()) { + debug.log("started QUIC idle timeout management for connection," + + " idle timeout event: " + this.idleTimeoutEvent + + " deadline: " + deadline); + } else { + Log.logQuic("{0} started QUIC idle timeout management for connection," + + " idle timeout event: {1} deadline: {2}", + connection.logTag(), this.idleTimeoutEvent, deadline); + } + } + + private void stopIdleTerminationTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + if (this.idleTimeoutEvent == null) { + return; + } + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + // disable the event (refreshDeadline() of IdleTimeoutEvent will return Deadline.MAX) + final Deadline nextDeadline = this.idleTimeoutEvent.nextDeadline; + if (!nextDeadline.equals(Deadline.MAX)) { + this.idleTimeoutEvent.nextDeadline = Deadline.MAX; + endpoint.timer().reschedule(this.idleTimeoutEvent, Deadline.MIN); + } + this.idleTimeoutEvent = null; + } + + private void startStreamDataBlockedTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + // 75% of idle timeout or if idle timeout is not configured, then 30 seconds + final long timeoutMillis = getIdleTimeout() + .map((v) -> (long) (0.75 * v)) + .orElse(30000L); + final QuicTimerQueue timerQueue = connection.endpoint().timer(); + final Deadline deadline = timeLine().instant().plusMillis(timeoutMillis); + // we don't expect the timer to be started more than once + assert this.streamDataBlockedEvent == null : "STREAM_DATA_BLOCKED timer already started"; + // create the timeout event and register with the QuicTimerQueue. + this.streamDataBlockedEvent = new StreamDataBlockedEvent(deadline, timeoutMillis); + timerQueue.offer(this.streamDataBlockedEvent); + if (debug.on()) { + debug.log("started STREAM_DATA_BLOCKED timer for connection," + + " event: " + this.streamDataBlockedEvent + + " deadline: " + deadline); + } else { + Log.logQuic("{0} started STREAM_DATA_BLOCKED timer for connection," + + " event: {1} deadline: {2}", + connection.logTag(), this.streamDataBlockedEvent, deadline); + } + } + + private void stopStreamDataBlockedTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + if (this.streamDataBlockedEvent == null) { + return; + } + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + // disable the event (refreshDeadline() of StreamDataBlockedEvent will return Deadline.MAX) + final Deadline nextDeadline = this.streamDataBlockedEvent.nextDeadline; + if (!nextDeadline.equals(Deadline.MAX)) { + this.streamDataBlockedEvent.nextDeadline = Deadline.MAX; + endpoint.timer().reschedule(this.streamDataBlockedEvent, Deadline.MIN); + } + this.streamDataBlockedEvent = null; + } + + /** + * Attempts to notify the idle connection management that this connection should + * be considered "in use". This way the idle connection management doesn't close + * this connection during the time the connection is handed out from the pool and any + * new stream created on that connection. + * + * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false + * otherwise; in which case the connection must not be handed out from the pool. + */ + boolean tryReserveForUse() { + this.idleTerminationLock.lock(); + try { + if (chosenForIdleTermination) { + // idle termination has been decided for this connection, don't use it + return false; + } + // if the connection is nearing idle timeout due to lack of traffic then + // don't use it + final long lastPktActivity = lastPacketActivityAt; + final long currentNanos = System.nanoTime(); + final long inactivityMs = MILLISECONDS.convert((currentNanos - lastPktActivity), + NANOSECONDS); + final boolean nearingIdleTimeout = getIdleTimeout() + .map((timeoutMillis) -> inactivityMs >= (0.8 * timeoutMillis)) // 80% of idle timeout + .orElse(false); + if (nearingIdleTimeout) { + return false; + } + // express interest in using the connection + this.lastUsageReservationAt = System.nanoTime(); + return true; + } finally { + this.idleTerminationLock.unlock(); + } + } + + + /** + * Returns the idle timeout duration, in milliseconds, negotiated for the connection represented + * by this {@code IdleTimeoutManager}. The negotiated idle timeout of a connection + * is the minimum of the idle connection timeout that is advertised by the + * endpoint represented by this {@code IdleTimeoutManager} and the idle + * connection timeout advertised by the peer. If neither endpoints have advertised + * any idle connection timeout then this method returns an + * {@linkplain Optional#empty() empty} value. + * + * @return the idle timeout in milliseconds or {@linkplain Optional#empty() empty} + */ + Optional getIdleTimeout() { + final long val = this.idleTimeoutDurationMs.get(); + return val == NO_IDLE_TIMEOUT ? Optional.empty() : Optional.of(val); + } + + void keepAlive() { + lastPacketActivityAt = System.nanoTime(); // TODO: timeline().instant()? + } + + void shutdown() { + if (!shutdown.compareAndSet(false, true)) { + // already shutdown + return; + } + this.stateLock.lock(); + try { + // unregister the timeout events from the QuicTimerQueue + stopIdleTerminationTimer(); + stopStreamDataBlockedTimer(); + } finally { + this.stateLock.unlock(); + } + if (debug.on()) { + debug.log("idle timeout manager shutdown"); + } + } + + void localIdleTimeout(final long timeoutMillis) { + checkUpdateIdleTimeout(timeoutMillis); + } + + void peerIdleTimeout(final long timeoutMillis) { + checkUpdateIdleTimeout(timeoutMillis); + } + + private void checkUpdateIdleTimeout(final long newIdleTimeoutMillis) { + if (newIdleTimeoutMillis <= 0) { + // idle timeout should be non-zero value, we disregard other values + return; + } + long current; + boolean updated = false; + // update the idle timeout if the new timeout is lesser + // than the previously set value + while ((current = this.idleTimeoutDurationMs.get()) == NO_IDLE_TIMEOUT + || current > newIdleTimeoutMillis) { + updated = this.idleTimeoutDurationMs.compareAndSet(current, newIdleTimeoutMillis); + if (updated) { + break; + } + } + if (!updated) { + return; + } + if (debug.on()) { + debug.log("idle connection timeout updated to " + + newIdleTimeoutMillis + " milli seconds"); + } else { + Log.logQuic("{0} idle connection timeout updated to {1} milli seconds", + connection.logTag(), newIdleTimeoutMillis); + } + } + + private TimeLine timeLine() { + return this.connection.endpoint().timeSource(); + } + + // called when the connection has been idle past its idle timeout duration + private void idleTimedOut() { + if (shutdown.get()) { + return; // nothing to do - the idle timeout manager has been shutdown + } + final Optional timeoutVal = getIdleTimeout(); + assert timeoutVal.isPresent() : "unexpectedly idle timing" + + " out connection, when no idle timeout is configured"; + final long timeoutMillis = timeoutVal.get(); + if (Log.quic() || debug.on()) { + // log idle timeout, with packet space statistics + final String msg = "silently terminating connection due to idle timeout (" + + timeoutMillis + " milli seconds)"; + StringBuilder sb = new StringBuilder(); + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (connection.packetNumberSpaces().get(sp) instanceof PacketSpaceManager m) { + sb.append("\n PacketSpace: ").append(sp).append('\n'); + m.debugState(" ", sb); + } + } + if (Log.quic()) { + Log.logQuic("{0} {1}: {2}", connection.logTag(), msg, sb.toString()); + } else if (debug.on()) { + debug.log("%s: %s", msg, sb); + } + } + // silently close the connection and discard all its state + final TerminationCause cause = forSilentTermination("connection idle timed out (" + + timeoutMillis + " milli seconds)"); + connection.terminator.terminate(cause); + } + + private long computeInactivityMillis() { + final long currentNanos = System.nanoTime(); + final long lastActiveNanos = Math.max(lastPacketActivityAt, lastUsageReservationAt); + return MILLISECONDS.convert((currentNanos - lastActiveNanos), NANOSECONDS); + } + + final class IdleTimeoutEvent implements QuicTimedEvent { + private final long eventId; + private volatile Deadline deadline; + private volatile Deadline nextDeadline; + + private IdleTimeoutEvent(final Deadline deadline) { + assert deadline != null : "timeout deadline is null"; + this.deadline = this.nextDeadline = deadline; + this.eventId = QuicTimerQueue.newEventId(); + } + + @Override + public Deadline deadline() { + return this.deadline; + } + + @Override + public Deadline refreshDeadline() { + if (shutdown.get()) { + return this.deadline = this.nextDeadline = Deadline.MAX; + } + return this.deadline = this.nextDeadline; + } + + @Override + public Deadline handle() { + if (shutdown.get()) { + // timeout manager is shutdown, nothing more to do + return this.nextDeadline = Deadline.MAX; + } + final Optional idleTimeout = getIdleTimeout(); + if (idleTimeout.isEmpty()) { + // nothing to do, don't reschedule + return Deadline.MAX; + } + final long idleTimeoutMillis = idleTimeout.get(); + // check whether the connection has indeed been idle for the idle timeout duration + idleTerminationLock.lock(); + try { + Deadline postponed = maybePostponeDeadline(idleTimeoutMillis); + if (postponed != null) { + // not idle long enough, reschedule + this.nextDeadline = postponed; + return postponed; + } + chosenForIdleTermination = true; + } finally { + idleTerminationLock.unlock(); + } + // the connection has been idle for the idle timeout duration, go + // ahead and terminate it. + terminateNow(); + assert shutdown.get() : "idle timeout manager was expected to be shutdown"; + this.nextDeadline = Deadline.MAX; + return Deadline.MAX; + } + + private Deadline maybePostponeDeadline(final long expectedIdleDurationMs) { + assert idleTerminationLock.isHeldByCurrentThread() : "not holding idle termination lock"; + final long inactivityMs = computeInactivityMillis(); + if (inactivityMs >= expectedIdleDurationMs) { + // the connection has been idle long enough, don't postpone the timeout. + return null; + } + // not idle long enough, compute the deadline when it's expected to reach + // idle timeout + final long remainingMs = expectedIdleDurationMs - inactivityMs; + final Deadline next = timeLine().instant().plusMillis(remainingMs); + if (debug.on()) { + debug.log("postponing timeout event: " + this + " to fire" + + " in " + remainingMs + " milli seconds, deadline: " + next); + } + return next; + } + + private void terminateNow() { + try { + idleTimedOut(); + } finally { + shutdown(); + } + } + + @Override + public long eventId() { + return this.eventId; + } + + @Override + public String toString() { + return "QuicIdleTimeoutEvent-" + this.eventId; + } + } + + final class StreamDataBlockedEvent implements QuicTimedEvent { + private final long eventId; + private final long timeoutMillis; + private volatile Deadline deadline; + private volatile Deadline nextDeadline; + + private StreamDataBlockedEvent(final Deadline deadline, final long timeoutMillis) { + assert deadline != null : "timeout deadline is null"; + this.deadline = this.nextDeadline = deadline; + this.timeoutMillis = timeoutMillis; + this.eventId = QuicTimerQueue.newEventId(); + } + + @Override + public Deadline deadline() { + return this.deadline; + } + + @Override + public Deadline refreshDeadline() { + if (shutdown.get()) { + return this.deadline = this.nextDeadline = Deadline.MAX; + } + return this.deadline = this.nextDeadline; + } + + @Override + public Deadline handle() { + if (shutdown.get()) { + // timeout manager is shutdown, nothing more to do + return this.nextDeadline = Deadline.MAX; + } + // check whether the connection has indeed been idle for the idle timeout duration + idleTerminationLock.lock(); + try { + if (chosenForIdleTermination) { + // connection is already chosen for termination, no need to send + // a STREAM_DATA_BLOCKED + this.nextDeadline = Deadline.MAX; + return this.nextDeadline; + } + final long inactivityMs = computeInactivityMillis(); + if (inactivityMs >= timeoutMillis && connection.streams.hasBlockedStreams()) { + // has been idle long enough, but there are streams that are blocked due to + // flow control limits and that could have lead to the idleness. + // trigger sending a STREAM_DATA_BLOCKED frame for the streams + // to try and have their limits increased by the peer. + connection.streams.enqueueStreamDataBlocked(); + if (debug.on()) { + debug.log("enqueued a STREAM_DATA_BLOCKED frame since connection" + + " has been idle due to blocked stream(s)"); + } else { + Log.logQuic("{0} enqueued a STREAM_DATA_BLOCKED frame" + + " since connection has been idle due to" + + " blocked stream(s)", connection.logTag()); + } + } + this.nextDeadline = timeLine().instant().plusMillis(timeoutMillis); + return this.nextDeadline; + } finally { + idleTerminationLock.unlock(); + } + } + + @Override + public long eventId() { + return this.eventId; + } + + @Override + public String toString() { + return "StreamDataBlockedEvent-" + this.eventId; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java new file mode 100644 index 00000000000..76a1f251e75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantLock; + +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * Manages the connection ids advertised by the local endpoint of a connection. + * - Produces outgoing NEW_CONNECTION_ID frames, + * - handles incoming RETIRE_CONNECTION_ID frames, + * - registers produced connection IDs with the QuicEndpoint + * Handshake connection ID is created and registered by QuicConnection. + */ +final class LocalConnIdManager { + private final Logger debug; + private final QuicConnectionImpl connection; + private long nextConnectionIdSequence; + private final ReentrantLock lock = new ReentrantLock(); + private boolean closed; // when true, no more connection IDs are registered + + // the connection ids (there can be more than one) with which the endpoint identifies this connection. + // the key of this Map is a (RFC defined) sequence number for the connection id + private final NavigableMap localConnectionIds = + Collections.synchronizedNavigableMap(new TreeMap<>()); + + LocalConnIdManager(final QuicConnectionImpl connection, final String dbTag, + QuicConnectionId handshakeConnectionId) { + this.debug = Utils.getDebugLogger(() -> dbTag); + this.connection = connection; + this.localConnectionIds.put(nextConnectionIdSequence++, handshakeConnectionId); + } + + private QuicConnectionId newConnectionId() { + return connection.endpoint().idFactory().newConnectionId(); + + } + + private byte[] statelessTokenFor(QuicConnectionId cid) { + return connection.endpoint().idFactory().statelessTokenFor(cid); + } + + void handleRetireConnectionIdFrame(final QuicConnectionId incomingPacketDestConnId, + final QuicPacket.PacketType packetType, + final RetireConnectionIDFrame retireFrame) + throws QuicTransportException { + if (debug.on()) { + debug.log("Received RETIRE_CONNECTION_ID frame: %s", retireFrame); + } + final QuicConnectionId toRetire; + lock.lock(); + try { + final long seqNumber = retireFrame.sequenceNumber(); + if (seqNumber >= nextConnectionIdSequence) { + // RFC-9000, section 19.16: Receipt of a RETIRE_CONNECTION_ID frame containing a + // sequence number greater than any previously sent to the peer MUST be treated + // as a connection error of type PROTOCOL_VIOLATION + throw new QuicTransportException("Invalid sequence number " + seqNumber + + " in RETIRE_CONNECTION_ID frame", + packetType.keySpace().orElse(null), + retireFrame.getTypeField(), PROTOCOL_VIOLATION); + } + toRetire = this.localConnectionIds.get(seqNumber); + if (toRetire == null) { + return; + } + if (toRetire.equals(incomingPacketDestConnId)) { + // RFC-9000, section 19.16: The sequence number specified in a RETIRE_CONNECTION_ID + // frame MUST NOT refer to the Destination Connection ID field of the packet in which + // the frame is contained. The peer MAY treat this as a connection error of type + // PROTOCOL_VIOLATION. + throw new QuicTransportException("Invalid connection id in RETIRE_CONNECTION_ID frame", + packetType.keySpace().orElse(null), + retireFrame.getTypeField(), PROTOCOL_VIOLATION); + } + // forget this id from our local store + this.localConnectionIds.remove(seqNumber); + this.connection.endpoint().removeConnectionId(toRetire, connection); + } finally { + lock.unlock(); + } + if (debug.on()) { + debug.log("retired connection id " + toRetire); + } + } + + public QuicFrame nextFrame(int remaining) { + if (localConnectionIds.size() >= 2) { + return null; + } + int cidlen = connection.endpoint().idFactory().connectionIdLength(); + if (cidlen == 0) { + return null; + } + // frame: + // type - 1 byte + // sequence number - var int + // retire prior to - 1 byte (always zero) + // connection id: + 1 byte + // stateless reset token - 16 bytes + int len = 19 + cidlen + VariableLengthEncoder.getEncodedSize(nextConnectionIdSequence); + if (len > remaining) { + return null; + } + NewConnectionIDFrame newCidFrame; + QuicConnectionId cid = newConnectionId(); + byte[] token = statelessTokenFor(cid); + lock.lock(); + try { + if (closed) return null; + newCidFrame = new NewConnectionIDFrame(nextConnectionIdSequence++, 0, + cid.asReadOnlyBuffer(), ByteBuffer.wrap(token)); + this.localConnectionIds.put(newCidFrame.sequenceNumber(), cid); + this.connection.endpoint().addConnectionId(cid, connection); + if (debug.on()) { + debug.log("Sending NEW_CONNECTION_ID frame"); + } + return newCidFrame; + } finally { + lock.unlock(); + } + } + + public List connectionIds() { + lock.lock(); + try { + // copy to avoid ConcurrentModificationException + return List.copyOf(localConnectionIds.values()); + } finally { + lock.unlock(); + } + } + + public void close() { + lock.lock(); + closed = true; + lock.unlock(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java new file mode 100644 index 00000000000..52821a0fac2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; + +/** + * A class to take care of frames reordering in an ordered flow. + * + * Frames that are {@linkplain #receive(QuicFrame) received} out of order + * will be either buffered or dropped, depending on their {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) position} + * with respect to the current ordered flow {@linkplain #offset() offset}. + * The buffered frames are returned by later calls to {@linkplain #poll()} + * when the flow offset matches the frame offset. + * + * Frames that are {@linkplain #receive(QuicFrame) received} in order + * are immediately returned. + * + * This class is not thread-safe and concurrent access needs to be synchronized + * externally. + * @param A frame type that defines an offset and a {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) + * length}. The offset of the frame + * indicates its {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) + * position} in the ordered flow. + */ +public sealed abstract class OrderedFlow { + + /** + * A subclass of {@link OrderedFlow} used to reorder instances of + * {@link CryptoFrame}. + */ + public static final class CryptoDataFlow extends OrderedFlow { + /** + * Constructs a new instance of {@code CryptoDataFlow} to reorder + * a flow of {@code CryptoFrame} instances. + */ + public CryptoDataFlow() { + super(CryptoFrame::compareOffsets, + CryptoFrame::offset, + CryptoFrame::length); + } + + @Override + protected CryptoFrame slice(CryptoFrame frame, long offset, int length) { + if (length == 0) return null; + return frame.slice(offset, length); + } + } + + /** + * A subclass of {@link OrderedFlow} used to reorder instances of + * {@link StreamFrame}. + */ + public static final class StreamDataFlow extends OrderedFlow { + /** + * Constructs a new instance of {@code StreamDataFlow} to reorder + * a flow of {@code StreamFrame} instances. + */ + public StreamDataFlow() { + super(StreamFrame::compareOffsets, + StreamFrame::offset, + StreamFrame::dataLength); + } + + @Override + protected StreamFrame slice(StreamFrame frame, long offset, int length) { + if (length == 0) return null; + return frame.slice(offset, length); + } + } + + private final ConcurrentSkipListSet queue; + private final ToLongFunction position; + private final ToIntFunction length; + long offset; + long buffered; + + /** + * Constructs a new instance of ordered flow to reorder frames in a given + * flow. + * @param comparator A comparator to order the frames according to their position in + * the ordered flow. Typically, this will compare the + * frame's offset: the frame with the smaller offset will be sorted + * before the frame with the greater offset + * @param position A method reference that returns the position of the frame in the + * flow. For instance, this would be {@link CryptoFrame#offset() + * CryptoFrame::offset} if {@code } is {@code CryptoFrame}, or + * {@link StreamFrame#offset() StreamFrame::offset} if {@code } + * is {@code StreamFrame} + * @param length A method reference that returns the number of bytes in the frame data. + * This is used to compute the expected position of the next + * frame in the flow. For instance, this would be {@link CryptoFrame#length() + * CryptoFrame::length} if {@code } is {@code CryptoFrame}, or + * {@link StreamFrame#dataLength() StreamFrame::dataLength} if {@code } + * is {@code StreamFrame} + */ + public OrderedFlow(Comparator comparator, ToLongFunction position, + ToIntFunction length) { + queue = new ConcurrentSkipListSet<>(comparator); + this.position = position; + this.length = length; + } + + /** + * {@return a slice of the given frame} + * @param frame the frame to slice + * @param offset the new frame offset + * @param length the new frame length + * @throws IndexOutOfBoundsException if the new offset or length + * fall outside of the frame's bounds + */ + protected abstract T slice(T frame, long offset, int length); + + /** + * Receives a new frame. If the frame is below the current + * offset the frame is dropped. If it is above the current offset, + * it is queued. + * If the frame is exactly at the current offset, it is + * returned. + * + * @param frame a frame that was received + * @return the next frame in the flow, or {@code null} if it is not + * available yet. + */ + public T receive(T frame) { + if (frame == null) return null; + + long start = this.position.applyAsLong(frame); + int length = this.length.applyAsInt(frame); + long end = start + length; + assert length >= 0; + assert start >= 0; + long offset = this.offset; + if (end <= offset || length == 0) { + // late arrival or empty frame. Just drop it; No overlap + // if we reach here! + return null; + } else if (start > offset) { + // the frame is after the offset. + // insert or slice it, depending on what we + // have already received. + enqueue(frame, start, length, offset); + return null; + } else { + // case where the frame is either at offset, or is below + // offset but has a length that provides bytes that + // overlap with the current offset. In the later case + // we will return a slice. + int todeliver = (int)(end - offset); + + assert end == offset + todeliver; + // update the offset with the new position + this.offset = end; + // cleanup the queue + dropuntil(end); + if (start == offset) return frame; + return slice(frame, offset, todeliver); + } + } + + private T peekFirst() { + if (queue.isEmpty()) return null; + // why is there no peekFirst? + try { + return queue.first(); + } catch (NoSuchElementException nse) { + return null; + } + } + + private void enqueue(T frame, long pos, int length, long after) { + assert pos == position.applyAsLong(frame); + assert length == this.length.applyAsInt(frame); + assert pos > after; + long offset = this.offset; + assert offset >= after; + long newpos = pos; + int newlen = length; + long limit = Math.addExact(pos, length); + + // look at the closest frame, if any, whose offset is <= to + // the new frame offset. Try to see if the new frame overlaps + // with that frame, and if so, drops the part that overlaps + // in the new frame. + T floor = queue.floor(frame); + if (floor != null) { + long foffset = position.applyAsLong(floor); + long flen = this.length.applyAsInt(floor); + if (limit <= foffset + flen) { + // bytes already all buffered! + // just drop the frame + return; + } + assert foffset <= pos; + // foffset == pos case handled as ceiling below + if (foffset < pos && pos - foffset < flen) { + // reduce the frame if it overlaps with the + // one that sits just before in the queue + newpos = foffset + flen; + newlen = length - (int) (newpos - pos); + } + } + assert limit == newpos + newlen; + + // Look at the frames that have an offset higher or equal to + // the new frame offset, and see if any overlap with the new + // frame. Remove frames that are entirely contained in the new one, + // slice the current frame if the frames overlap. + while (true) { + T ceil = queue.ceiling(frame); + if (ceil != null) { + long coffset = position.applyAsLong(ceil); + assert coffset >= newpos : "overlapping frames in queue"; + if (coffset < limit) { + long clen = this.length.applyAsInt(ceil); + if (clen <= limit - coffset) { + // ceiling frame completely contained in the new frame: + // remove the ceiling frame + queue.remove(ceil); + buffered -= clen; + continue; + } + // safe cast, since newlen <= len + newlen = (int) (coffset - newpos); + } + } + break; + } + assert newlen >= 0; + if (newlen == length) { + assert newpos == pos; + queue.add(frame); + } else if (newlen > 0) { + queue.add(slice(frame, newpos, newlen)); + } + buffered += newlen; + } + + /** + * Removes and return the head of the queue if it is at the + * current offset. Otherwise, returns null. + * @return the head of the queue if it is at the current offset, + * or {@code null} + */ + public T poll() { + return poll(offset); + } + + /** + * {@return the number of buffered frames} + */ + public int size() { + return queue.size(); + } + + /** + * {@return the number of bytes buffered} + */ + public long buffered() { + return buffered; + } + + /** + * {@return true if there are no buffered frames} + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * {@return the current offset of this buffer} + */ + public long offset() { + return offset; + } + + /** + * Drops all buffered frames + */ + public void clear() { + queue.clear(); + } + + /** + * Drop all frames in the buffer whose position is strictly + * below offset. + * + * @param offset the offset below which frames should be dropped + * @return the amount of dropped data + */ + private long dropuntil(long offset) { + T head; + long pos; + long dropped = 0; + do { + head = peekFirst(); + if (head == null) break; + pos = position.applyAsLong(head); + if (pos < offset) { + var length = this.length.applyAsInt(head); + var consumed = offset - pos; + if (length <= consumed) { + // drop it + if (head == queue.pollFirst()) { + buffered -= length; + dropped += length; + } else { + throw new AssertionError("Concurrent modification"); + } + } else { + // safe cast: consumed < length if we reach here + int newlen = length - (int)consumed; + var newhead = slice(head, offset, newlen); + if (head == queue.pollFirst()) { + queue.add(newhead); + buffered -= consumed; + dropped += consumed; + } else { + throw new AssertionError("Concurrent modification"); + } + } + } + } while (pos < offset); + return dropped; + } + + /** + * Pretends to {@linkplain #receive(QuicFrame) receive} the head of the queue, + * if it is at the provided offset + * + * @param offset the minimal offset + * + * @return a received frame at the current flow offset, or {@code null} + */ + private T poll(long offset) { + long current = this.offset; + assert offset <= current; + dropuntil(offset); + T head = peekFirst(); + if (head != null) { + long pos = position.applyAsLong(head); + if (pos == offset) { + // the frame we wanted was in the queue! + // well, let's handle it... + if (head == queue.pollFirst()) { + long length = this.length.applyAsInt(head); + buffered -= length; + } else { + throw new AssertionError("Concurrent modification"); + } + return receive(head); + } + } + return null; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java new file mode 100644 index 00000000000..35b42333a1e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.concurrent.Executor; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTransportException; + +/** + * This interface is a useful abstraction used to tie + * {@link PacketSpaceManager} and {@link QuicConnectionImpl}. + * The {@link PacketSpaceManager} uses functionalities provided + * by a {@link PacketEmitter} when it deems that a packet needs + * to be retransmitted, or that an acknowledgement is due. + * It also uses the emitter's {@linkplain #timer() timer facility} + * when it needs to register a {@link QuicTimedEvent}. + * + * @apiNote + * All these methods are actually implemented by {@link QuicConnectionImpl} + * but the {@code PacketEmitter} interface makes it possible to write + * unit tests against a {@link PacketSpaceManager} without involving + * any {@code QuicConnection} instance. + * + */ +public interface PacketEmitter { + /** + * {@return the timer queue used by this packet emitter} + */ + QuicTimerQueue timer(); + + /** + * Retransmit the given packet on behalf of the given packet space + * manager. + * @param packetSpaceManager the packet space manager on behalf of + * which the packet is being retransmitted + * @param packet the unacknowledged packet which should be retransmitted + * @param attempts the number of previous retransmission of this packet. + * A value of 0 indicates the first retransmission. + */ + void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Emit a possibly non ACK-eliciting packet containing the given ACK frame. + * @param packetSpaceManager the packet space manager on behalf + * of which the acknowledgement should + * be sent. + * @param ackFrame the ACK frame to be sent. + * @param sendPing whether a PING frame should be sent. + * @return the emitted packet number, or -1L if not applicable or not emitted + */ + long emitAckPacket(PacketSpace packetSpaceManager, AckFrame ackFrame, boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Called when a packet has been acknowledged. + * @param packet the acknowledged packet + */ + void acknowledged(QuicPacket packet); + + /** + * Called when congestion controller allows sending one packet + * @param packetNumberSpace current packet number space + * @return true if a packet was sent, false otherwise + */ + boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * {@return an executor to use when {@linkplain + * jdk.internal.net.http.common.SequentialScheduler#runOrSchedule(Executor) + * offloading loops to another thread} is required} + */ + Executor executor(); + + /** + * Reschedule the given event on the {@link #timer() timer} + * @param event the event to reschedule + */ + default void reschedule(QuicTimedEvent event) { + timer().reschedule(event); + } + + /** + * Reschedule the given event on the {@link #timer() timer} + * @param event the event to reschedule + */ + default void reschedule(QuicTimedEvent event, Deadline deadline) { + timer().reschedule(event, deadline); + } + + /** + * Abort the connection if needed, for example if the peer is not responding + * or max idle time was reached + */ + void checkAbort(PacketNumberSpace packetNumberSpace); + + /** + * {@return true if this emitter is open for transmitting packets, else returns false} + */ + boolean isOpen(); + + default void ptoBackoffIncreased(PacketSpaceManager space, long backoff) { }; + + default String logTag() { return toString(); } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java new file mode 100644 index 00000000000..494af85447e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java @@ -0,0 +1,2370 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckFrameBuilder; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A {@code PacketSpaceManager} takes care of acknowledgement and + * retransmission of packets for a given {@link PacketNumberSpace}. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ + +// See also: RFC 9000, https://www.rfc-editor.org/rfc/rfc9000#name-sending-ack-frames +// Every packet SHOULD be acknowledged at least once, and +// ack-eliciting packets MUST be acknowledged at least once within +// the maximum delay an endpoint communicated using the max_ack_delay +// transport parameter [...]; +// [...] +// In order to assist loss detection at the sender, an endpoint +// SHOULD generate and send an ACK frame without delay when it +// receives an ack-eliciting packet either: +// - when the received packet has a packet number less +// than another ack-eliciting packet that has been received, or +// - when the packet has a packet number larger than the +// highest-numbered ack-eliciting packet that has been received +// and there are missing packets between that packet and this +// packet. [...] +public sealed class PacketSpaceManager implements PacketSpace + permits PacketSpaceManager.OneRttPacketSpaceManager, + PacketSpaceManager.HandshakePacketSpaceManager { + + private final QuicCongestionController congestionController; + private volatile boolean blockedByCC; + // packet threshold for loss detection; RFC 9002 suggests 3 + private static final long kPacketThreshold = 3; + // Multiplier for persistent congestion; RFC 9002 suggests 3 + private static final int kPersistentCongestionThreshold = 3; + + /** + * A record that stores the next AckFrame that should be sent + * within this packet number space. + * + * @param ackFrame the ACK frame to send. + * @param deadline the deadline by which to send this ACK frame. + * @param lastUpdated the time at which the {@link AckFrame}'s + * {@link AckFrame#largestAcknowledged()} was + * last updated. Used for calculating ack delay. + * @param sent the time at which the {@link AckFrame} was sent, + * or {@code null} if it has not been sent yet. + */ + record NextAckFrame(AckFrame ackFrame, + Deadline deadline, + Deadline lastUpdated, + Deadline sent) { + /** + * {@return an identical {@code NextAckFrame} record, with an updated + * {@code deadline}} + * @param deadline the new deadline + * @param sent the point in time at which the ack frame was sent, or null. + */ + public NextAckFrame withDeadline(Deadline deadline, Deadline sent) { + return new NextAckFrame(ackFrame, deadline, lastUpdated, sent); + } + } + + // true if transmit timer should fire now + private volatile boolean transmitNow; + + // These two numbers control whether an PING frame will be + // sent with the next ACK frame, to turn the packet that + // contains the ACK frame into an ACK-eliciting packet. + // These numbers are *not* defined in RFC 9000, but are used + // to implement a strategy for sending occasional PING frames + // in order to prevent ACK frames from growing too big. + // See RFC 9000 section 13.2.4 + // https://www.rfc-editor.org/rfc/rfc9000#name-limiting-ranges-by-tracking + public static final int MAX_ACKRANGE_COUNT_BEFORE_PING = 10; + + protected final Logger debug; + private final Supplier debugStrSupplier; + private final PacketNumberSpace packetNumberSpace; + private final PacketEmitter packetEmitter; + private final ReentrantLock transferLock = new ReentrantLock(); + // The next packet number to use in this space + private final AtomicLong nextPN = new AtomicLong(); + private final TimeLine instantSource; + private final QuicRttEstimator rttEstimator; + // first packet number sent after handshake confirmed + private long handshakeConfirmedPN; + + // A priority queue containing a record for each unacknowledged PingRequest. + // PingRequest are removed from this queue when they are acknowledged, that + // is when any packet whose number is greater than the request packet + // is acknowledged. + // Note: this is used to implement {@link #requestSendPing()} which is + // used to implement out of band ping requests triggered by the + // application. + private final ConcurrentLinkedQueue pendingPingRequests = + new ConcurrentLinkedQueue<>(); + + // A priority queue containing a record for each unacknowledged packet. + // Packets are removed from this queue when they are acknowledged, or when they + // are being retransmitted. In which case, they will be in the pendingRetransmission + // queue + private final ConcurrentLinkedQueue pendingAcknowledgements = + new ConcurrentLinkedQueue<>(); + + // A map containing send times of ack-eliciting packets. + // Packets are removed from this map when they can't contribute to RTT sample, + // i.e. when they are acknowledged, or when a higher-numbered packet is acknowledged. + private final ConcurrentSkipListMap sendTimes = + new ConcurrentSkipListMap<>(); + + // A priority queue containing a record for each unacknowledged packet whose deadline + // is due, and which is currently being retransmitted. + // Packets are removed from this queue when they have been scheduled for retransmission + // with the quic endpoint + private final ConcurrentLinkedQueue pendingRetransmission = + new ConcurrentLinkedQueue<>(); + + // A priority queue containing a record for each unacknowledged packet whose deadline + // is due, and which should be retransmitted. + // Packets are removed from this queue when they have been scheduled for encryption. + private final ConcurrentLinkedQueue triggeredForRetransmission = + new ConcurrentLinkedQueue<>(); + + // lost packets + private final ConcurrentLinkedQueue lostPackets = + new ConcurrentLinkedQueue<>(); + + // A task invoked by the QuicTimerQueue when some packet retransmission are + // due. This task will move packets from the pendingAcknowledgement queue + // into the triggeredForRetransmission queue (and pendingRetransmission queue) + private final PacketTransmissionTask packetTransmissionTask; + // Used to synchronize transmission with handshake restarts + private final ReentrantLock transmitLock = new ReentrantLock(); + private volatile boolean fastRetransmitDone; + private volatile boolean fastRetransmit; + + /** + * A record to store previous numbers with which a packet has been + * retransmitted. If such a packet is acknowledged, we can stop + * retransmission. + * + * @param number A packet number with which the content of this + * packet was previously sent. + * @param largestAcknowledged the largest packet number acknowledged by this + * previous packet, or {@code -1L} if no packet was + * acknowledged by this packet. + * @param previous Further previous packet numbers, or {@code null}. + */ + private record PreviousNumbers(long number, + long largestAcknowledged, PreviousNumbers previous) {} + + /** + * A record used to implement {@link #requestSendPing()}. + * @param sent when the ping frame was sent + * @param packetNumber the packet number of the packet containing the pingframe + * @param response the response, which will be complete as soon as a packet whose number is + * >= to {@code packetNumber} is received. + */ + private record PingRequest(Deadline sent, long packetNumber, CompletableFuture response) {} + + /** + * A record to store a packet that hasn't been acknowledged, and should + * be scheduled for retransmission if not acknowledged when the deadline + * is reached. + * + * @param packet the unacknowledged quic packet + * @param sent the instant when the packet was sent. + * @param packetNumber the packet number of the {@code packet} + * @param largestAcknowledged the largest packet number acknowledged by this + * packet, or {@code -1L} if no packet is acknowledged + * by this packet. + * @param previousNumbers previous packet numbers with which the packet was + * transmitted, if any, {@code null} otherwise. + */ + private record PendingAcknowledgement(QuicPacket packet, Deadline sent, + long packetNumber, long largestAcknowledged, + PreviousNumbers previousNumbers) { + + PendingAcknowledgement(QuicPacket packet, Deadline sent, + long packetNumber, PreviousNumbers previousNumbers) { + this(packet, sent, packetNumber, + AckFrame.largestAcknowledgedInPacket(packet), previousNumbers); + } + + boolean hasPreviousNumber(long packetNumber) { + if (this.packetNumber <= packetNumber) return false; + var pn = previousNumbers; + while (pn != null) { + if (pn.number == packetNumber) { + return true; + } + pn = pn.previous; + } + return false; + } + boolean hasExactNumber(long packetNumber) { + return this.packetNumber == packetNumber; + } + boolean hasNumber(long packetNumber) { + return this.packetNumber == packetNumber || hasPreviousNumber(packetNumber); + } + PreviousNumbers findPreviousAcknowledged(AckFrame frame) { + var pn = previousNumbers; + while (pn != null) { + if (frame.isAcknowledging(pn.number)) return pn; + pn = pn.previous; + } + return null; + } + boolean isAcknowledgedBy(AckFrame frame) { + if (frame.isAcknowledging(packetNumber)) return true; + else return findPreviousAcknowledged(frame) != null; + } + + public int attempts() { + var pn = previousNumbers; + int count = 0; + while (pn != null) { + count++; + pn = pn.previous; + } + return count; + } + + String prettyPrint() { + StringBuilder b = new StringBuilder(); + b.append("pn:").append(packetNumber); + var ppn = previousNumbers; + if (ppn != null) { + var sep = " ["; + while (ppn != null) { + b.append(sep).append(ppn.number); + ppn = ppn.previous; + sep = ", "; + } + b.append("]"); + } + return b.toString(); + } + } + + /** + * A task that sends packets to the peer. + * + * Packets are sent after a delay when: + * - ack delay timer expires + * - PTO timer expires + * They can also be sent without delay when: + * - we are unblocked by the peer + * - new data is available for sending, and we are not blocked + * - need to send ack without delay + */ + final class PacketTransmissionTask implements QuicTimedEvent { + private final SequentialScheduler handleScheduler = + SequentialScheduler.lockingScheduler(this::handleLoop); + private final long id = QuicTimerQueue.newEventId(); + private volatile Deadline nextDeadline; // updated through VarHandle + private PacketTransmissionTask() { + nextDeadline = Deadline.MAX; + } + + @Override + public long eventId() { return id; } + + @Override + public Deadline deadline() { + return nextDeadline; + } + + @Override + public Deadline handle() { + if (closed) { + if (debug.on()) { + debug.log("packet space already closed, PacketTransmissionTask will" + + " no longer be scheduled"); + } + return Deadline.MAX; + } + handleScheduler.runOrSchedule(packetEmitter.executor()); + return Deadline.MAX; + } + + /** + * The handle loop takes care of sending ACKs, packaging stream data + * (if applicable), and retransmitting on PTO. It is never invoked + * directly - but can be triggered by {@link #handle()} or {@link + * #runTransmitter()} + */ + private void handleLoop() { + transmitLock.lock(); + try { + handleLoop0(); + } catch (Throwable t) { + if (Log.errors()) { + Log.logError("{0}: {1} handleLoop failed: {2}", + packetEmitter.logTag(), packetNumberSpace, t); + Log.logError(t); + } else if (debug.on()) { + debug.log("handleLoop failed", t); + } + } finally { + transmitLock.unlock(); + } + } + + private void handleLoop0() throws IOException, QuicTransportException { + // while congestion control allows, or if PTO expired: + // - send lost packet or new packet + // if PTO still expired (== nothing was sent) + // - resend oldest packet, if available + // - otherwise send ping (+ack, if available) + // if ACK still not sent, send ack + if (debug.on()) { + debug.log("PacketTransmissionTask::handle"); + } + packetEmitter.checkAbort(PacketSpaceManager.this.packetNumberSpace); + // Handle is called from within the executor + var nextDeadline = this.nextDeadline; + Deadline now = now(); + do { + transmitNow = false; + var closed = !isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("PacketTransmissionTask::handle: %s closed", + PacketSpaceManager.this.packetNumberSpace); + } + return; + } + if (debug.on()) debug.log("PacketTransmissionTask::handle"); + // this may update congestion controller + int lost = detectAndRemoveLostPackets(now); + if (lost > 0 && debug.on()) debug.log("handle: found %s lost packets", lost); + // if we're sending on PTO, we need to double backoff afterwards + boolean needBackoff = isPTO(now); + int packetsSent = 0; + boolean cwndAvailable; + while ((cwndAvailable = congestionController.canSendPacket()) || + (needBackoff && packetsSent < 2)) { // if PTO, try to send 2 packets + if (!isOpenForTransmission()) { + break; + } + final boolean retransmitted; + try { + retransmitted = retransmit(); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not re-transmitting any more data"); + } + clearAll(); + return; + } + throw new IOException("failed to retransmit data, reason: " + + qkue.getMessage()); + } + if (retransmitted) { + packetsSent++; + continue; + } + final boolean sentNew; + // nothing was retransmitted - check for new data + try { + sentNew = sendNewData(); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not transmitting any more data"); + } + return; + } + throw new IOException("failed to send new data, reason: " + + qkue.getMessage()); + } + if (!sentNew) { + break; + } else { + if (needBackoff && packetsSent == 0 && Log.quicRetransmit()) { + Log.logQuic("%s OUT: transmitted new packet on PTO".formatted( + packetEmitter.logTag())); + } + } + packetsSent++; + } + blockedByCC = !cwndAvailable; + if (!cwndAvailable && isOpenForTransmission()) { + if (debug.on()) debug.log("handle: blocked by CC"); + // CC might be available already + if (congestionController.canSendPacket()) { + if (debug.on()) debug.log("handle: unblocked immediately"); + transmitNow = true; + } + } + try { + if (isPTO(now) && isOpenForTransmission()) { + if (debug.on()) debug.log("handle: retransmit on PTO"); + // nothing was sent by the above loop - try to resend the oldest packet + retransmitPTO(); + } else if (fastRetransmit) { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + fastRetransmitDone = true; + fastRetransmit = false; + if (debug.on()) debug.log("handle: fast retransmit"); + // try to resend the oldest packet + retransmitPTO(); + } + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not re-transmitting any more data"); + } + return; + } + throw new IOException("failed to retransmitPTO data, key space, reason: " + + qkue.getMessage()); + } + boolean stillPTO = isPTO(now); + // if the ack frame is not sent yet, send it now + var ackFrame = getNextAckFrame(!stillPTO); + var pingRequested = PacketSpaceManager.this.pingRequested; + boolean sendPing = pingRequested != null || stillPTO + || shouldSendPing(now, ackFrame); + if (sendPing || ackFrame != null) { + if (debug.on()) debug.log("handle: generate ACK packet or PING ack:%s ping:%s", + ackFrame != null, sendPing); + final long emitted; + try { + emitted = emitAckPacket(ackFrame, sendPing); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not sending ack/ping packet"); + } + return; + } + throw new IOException("failed to send ack/ping data, reason: " + + qkue.getMessage()); + } + if (sendPing && pingRequested != null) { + if (emitted < 0) pingRequested.complete(-1L); + else registerPingRequest(new PingRequest(now, emitted, pingRequested)); + synchronized (PacketSpaceManager.this) { + PacketSpaceManager.this.pingRequested = null; + } + } + } + if (needBackoff) { + long backoff = rttEstimator.increasePtoBackoff(); + if (debug.on()) { + debug.log("handle: %s increase backoff to %s", + PacketSpaceManager.this.packetNumberSpace, + backoff); + } + packetEmitter.ptoBackoffIncreased(PacketSpaceManager.this, backoff); + } + + // if nextDeadline is not Deadline.MAX the task will be + // automatically rescheduled. + if (debug.on()) debug.log("handle: refreshing deadline"); + nextDeadline = computeNextDeadline(); + } while(!nextDeadline.isAfter(now)); + + logNoDeadline(nextDeadline, true); + if (Deadline.MAX.equals(nextDeadline)) return; + // we have a new deadline + packetEmitter.reschedule(this, nextDeadline); + } + + /** + * Create and send a new packet + * @return true if packet was sent, false if there is no more data to send + */ + private boolean sendNewData() throws QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) debug.log("handle: sending data..."); + boolean sent = packetEmitter.sendData(packetNumberSpace); + if (!sent) { + if (debug.on()) debug.log("handle: no more data to send"); + } + return sent; + } + + @Override + public Deadline refreshDeadline() { + Deadline previousDeadline, newDeadline; + do { + previousDeadline = this.nextDeadline; + newDeadline = computeNextDeadline(); + } while (!Handles.DEADLINE.compareAndSet(this, previousDeadline, newDeadline)); + + if (!newDeadline.equals(previousDeadline)) { + if (debug.on()) { + var now = now(); + if (newDeadline.equals(Deadline.MAX)) { + debug.log("Deadline refreshed: no new deadline"); + } else if (newDeadline.equals(Deadline.MIN)) { + debug.log("Deadline refreshed: run immediately"); + } else if (previousDeadline.equals(Deadline.MAX) || previousDeadline.equals(Deadline.MIN)) { + var delay = now.until(newDeadline, ChronoUnit.MILLIS); + if (delay < 0) { + debug.log("Deadline refreshed: new deadline passed by %dms", delay); + } else { + debug.log("Deadline refreshed: new deadline in %dms", delay); + } + } else { + var delay = now.until(newDeadline, ChronoUnit.MILLIS); + if (delay < 0) { + debug.log("Deadline refreshed: new deadline passed by %dms (diff: %dms)", + delay, previousDeadline.until(newDeadline, ChronoUnit.MILLIS)); + } else { + debug.log("Deadline refreshed: new deadline in %dms (diff: %dms)", + instantSource.instant().until(newDeadline, ChronoUnit.MILLIS), + previousDeadline.until(newDeadline, ChronoUnit.MILLIS)); + } + } + } + } else { + debug.log("Deadline not refreshed: no change"); + } + logNoDeadline(newDeadline, false); + return newDeadline; + } + + void logNoDeadline(Deadline newDeadline, boolean onlyNoDeadline) { + if (Log.quicRetransmit()) { + if (Deadline.MAX.equals(newDeadline)) { + if (shouldLogWhenNoDeadline()) { + Log.logQuic("{0}: {1} no deadline, task unscheduled", + packetEmitter.logTag(), packetNumberSpace); + } // else: no changes... + } else if (!onlyNoDeadline && shouldLogWhenNewDeadline()) { + if (Deadline.MIN.equals(newDeadline)) { + Log.logQuic("{0}: {1} Deadline.MIN, task will be rescheduled immediately", + packetEmitter.logTag(), packetNumberSpace); + } else { + try { + Log.logQuic("{0}: {1} new deadline computed, deadline in {2}ms", + packetEmitter.logTag(), packetNumberSpace, + Long.toString(now().until(newDeadline, ChronoUnit.MILLIS))); + } catch (ArithmeticException ae) { + Log.logError("Unexpected exception while logging deadline " + + newDeadline + ": " + ae); + Log.logError(ae); + assert false : "Unexpected ArithmeticException: " + ae; + } + } + } + } + } + + private boolean hadNoDeadline; + private synchronized boolean shouldLogWhenNoDeadline() { + if (!hadNoDeadline) { + hadNoDeadline = true; + return true; + } + return false; + } + + private synchronized boolean shouldLogWhenNewDeadline() { + if (hadNoDeadline) { + hadNoDeadline = false; + return true; + } + return false; + } + + boolean hasNoDeadline() { + return Deadline.MAX.equals(nextDeadline); + } + + // reschedule this task + void reschedule() { + Deadline deadline = computeNextDeadline(); + Deadline nextDeadline = this.nextDeadline; + if (Deadline.MAX.equals(deadline)) { + debug.log("no deadline, don't reschedule"); + } else if (deadline.equals(nextDeadline)) { + debug.log("deadline unchanged, don't reschedule"); + } else { + packetEmitter.reschedule(this, deadline); + debug.log("retransmission task: rescheduled"); + } + } + + @Override + public String toString() { + return "PacketTransmissionTask(" + debugStrSupplier.get() + ")"; + } + } + + Deadline deadline() { + return packetTransmissionTask.deadline(); + } + + Deadline prospectiveDeadline() { + return computeNextDeadline(false); + } + + // remove all pending acknowledgements and retransmissions. + private void clearAll() { + transferLock.lock(); + try { + pendingAcknowledgements.forEach(ack -> congestionController.packetDiscarded(List.of(ack.packet))); + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + pendingAcknowledgements.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting pending acks: " + sb); + } + } + pendingAcknowledgements.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + pendingRetransmission.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting pending retransmissions: " + sb); + } + } + pendingRetransmission.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + triggeredForRetransmission.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting triggered-for-retransmissions: " + sb.toString()); + } + } + triggeredForRetransmission.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + lostPackets.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting lost-packets: " + sb.toString()); + } + } + lostPackets.clear(); + } finally { + transferLock.unlock(); + } + } + + private void retransmitPTO() throws QuicKeyUnavailableException, QuicTransportException { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; retransmission on PTO dropped", packetNumberSpace); + } + clearAll(); + return; + } + + PendingAcknowledgement pending; + transferLock.lock(); + try { + if ((pending = pendingAcknowledgements.poll()) != null) { + if (debug.on()) debug.log("Retransmit on PTO: looking for candidate"); + // TODO should keep this packet on the list until it's either acked or lost + congestionController.packetDiscarded(List.of(pending.packet)); + pendingRetransmission.add(pending); + } + } finally { + transferLock.unlock(); + } + if (pending != null) { + packetEmitter.retransmit(this, pending.packet(), pending.attempts()); + } + } + + /** + * {@return true if this packet space isn't closed and if the underlying packet emitter + * is open, else returns false} + */ + private boolean isOpenForTransmission() { + return !this.closed && this.packetEmitter.isOpen(); + } + + /** + * A class to keep track of the largest packet that was acknowledged by + * a packet that is being acknowledged. + * This information is used to implement the algorithm described in + * RFC 9000 13.2.4. Limiting Ranges by Tracking ACK Frames + */ + private final class EmittedAckTracker { + volatile long ignoreAllPacketsBefore = -1; + /** + * Record the {@link AckFrame#largestAcknowledged() + * largest acknowledged} packet that was sent in an + * {@link AckFrame} that the peer has acknowledged. + * @param largestAcknowledged the packet number to record + * @return the largest {@code largestAcknowledged} + * packet number that was recorded. + * This is necessarily smaller than (or equal to) the + * {@link #getLargestSentAckedPN()}. + */ + private long record(long largestAcknowledged) { + long witness; + long largestSentAckedPN = PacketSpaceManager.this.largestSentAckedPN; + do { + witness = largestAckedPNReceivedByPeer; + if (witness >= largestAcknowledged) { + largestSentAckedPN = PacketSpaceManager.this.largestSentAckedPN; + assert witness <= largestSentAckedPN || ignoreAllPacketsBefore > largestSentAckedPN + : "largestAckedPNReceivedByPeer: %s, ignoreAllPacketsBefore: %s, largestSentAckedPN: %s" + .formatted(witness, ignoreAllPacketsBefore, largestSentAckedPN); + return witness; + } + } while (!Handles.LARGEST_ACK_ACKED_PN.compareAndSet( + PacketSpaceManager.this, witness, largestAcknowledged)); + assert witness <= largestAcknowledged; + assert largestAcknowledged <= largestSentAckedPN || ignoreAllPacketsBefore > largestSentAckedPN + : "largestAcknowledged: %s, ignoreAllPacketsBefore: %s, largestSentAckedPN: %s" + .formatted(largestSentAckedPN, ignoreAllPacketsBefore, largestSentAckedPN); + return largestAcknowledged; + } + + private boolean ignoreAllPacketsBefore(long packetNumber) { + long ignoreAllPacketsBefore; + do { + ignoreAllPacketsBefore = this.ignoreAllPacketsBefore; + if (packetNumber <= ignoreAllPacketsBefore) return false; + } while (!Handles.IGNORE_ALL_PN_BEFORE.compareAndSet( + this, ignoreAllPacketsBefore, packetNumber)); + return true; + } + + /** + * Tracks the largest packet acknowledged by the packets acknowledged in the + * given AckFrame. This helps to implement the algorithm described in + * RFC 9000, 13.2.4. Limiting Ranges by Tracking ACK Frames. + * @param pending a yet unacknowledged packet that may be acknowledged + * by the given{@link AckFrame}. + * @param frame a received {@code AckFrame} + * @return whether the given pending unacknowledged packet is being + * acknowledged by this ack frame. + */ + public boolean trackAcknowlegment(PendingAcknowledgement pending, AckFrame frame) { + if (frame.isAcknowledging(pending.packetNumber)) { + record(pending.largestAcknowledged); + packetEmitter.acknowledged(pending.packet()); + return true; + } + // There is a potential for a never ending retransmission + // loop here if we don't treat the ack of a previous packet just + // as the ack of the tip of the chain. + // So we call packetEmitter.acknowledged(pending.packet()) here too, + // and return `true` in this case as well. + var previous = pending.findPreviousAcknowledged(frame); + if (previous != null) { + record(previous.largestAcknowledged); + packetEmitter.acknowledged(pending.packet()); + return true; + } + return false; + } + + public long largestAckAcked() { + return largestAckedPNReceivedByPeer; + } + + public void dropPacketNumbersSmallerThan(long newLargestIgnored) { + // this method is called after arbitrarily reducing the ack range + // to this value; This mean we will drop packets whose packet + // number is smaller than the given packet number. + if (ignoreAllPacketsBefore(newLargestIgnored)) { + record(newLargestIgnored); + } + } + } + + private final QuicTLSEngine quicTLSEngine; + private final EmittedAckTracker emittedAckTracker; + private volatile NextAckFrame nextAckFrame; // assigned through VarHandle + // exponent for outgoing packets; defaults to 3 + public static final int ACK_DELAY_EXPONENT = 3; + // max ack delay sent in quic transport parameters, in millis + public static final int ADVERTISED_MAX_ACK_DELAY = 25; + // max timer delay, i.e. how late selector.select returns; 15.6 millis on Windows + public static final int TIMER_DELAY = 16; + // effective max ack delay for outgoing application packets + public static final int MAX_ACK_DELAY = ADVERTISED_MAX_ACK_DELAY - TIMER_DELAY; + + // exponent for incoming packets + private volatile long peerAckDelayExponent; + // max peer ack delay; zero on initial and handshake, + // initialized from transport parameters on application + private volatile long peerMaxAckDelayMillis; // ms + // max ack delay; zero on initial and handshake, MAX_ACK_DELAY on application + private final long maxAckDelay; // ms + volatile boolean closed; + + // The last time an ACK eliciting packet was sent. + // May be null before any such packet is sent... + private volatile Deadline lastAckElicitingTime; + + // not null if sending ping has been requested. + private volatile CompletableFuture pingRequested; + + // The largest packet number successfully processed in this space. + // Needed to decode received packet numbers, see RFC 9000 appendix A.3 + private volatile long largestProcessedPN; // assigned through VarHandle + + // The largest ACK-eliciting packet number received in this space. + // Needed to determine if we should send ACK without delay, see RFC 9000 section 13.2.1 + private volatile long largestAckElicitingReceivedPN; // assigned through VarHandle + + // The largest ACK-eliciting packet number sent in this space. + // Needed to determine if we should arm PTO timer + private volatile long largestAckElicitingSentPN; + + // The largest packet number acknowledged by peer. + // Needed to determine packet number length, see RFC 9000 appendix A.2 + private volatile long largestReceivedAckedPN; // assigned through VarHandle + + // The largest packet number acknowledged in this space + // This is the largest packet number we have acknowledged. + // This should be less or equal to the largestProcessedPN always. + // Not used. + private volatile long largestSentAckedPN; // assigned through VarHandle + + // The largest packet number that this instance has included + // in an AckFrame sent to the peer, and of which the peer has + // acknowledged reception. + // Used to limit ack ranges, see RFC 9000 section 13.2.4 + private volatile long largestAckedPNReceivedByPeer; // assigned through VarHandle + + /** + * Creates a new {@code PacketSpaceManager} for the given + * packet number space. + * @param connection The connection for which this manager + * is created. + * @param packetNumberSpace The packet number space. + */ + public PacketSpaceManager(final QuicConnectionImpl connection, + final PacketNumberSpace packetNumberSpace) { + this(packetNumberSpace, connection.emitter(), TimeSource.source(), + connection.rttEstimator, connection.congestionController, connection.getTLSEngine(), + () -> connection.dbgTag() + "[" + packetNumberSpace.name() + "]"); + } + + /** + * Creates a new {@code PacketSpaceManager} for the given + * packet number space. + * + * @param packetNumberSpace the packet number space. + * @param packetEmitter the packet emitter + * @param congestionController the congestion controller + * @param debugStrSupplier a supplier for a debug tag to use for logging purposes + */ + public PacketSpaceManager(PacketNumberSpace packetNumberSpace, + PacketEmitter packetEmitter, + TimeLine instantSource, + QuicRttEstimator rttEstimator, + QuicCongestionController congestionController, + QuicTLSEngine quicTLSEngine, + Supplier debugStrSupplier) { + largestProcessedPN = largestReceivedAckedPN = largestAckElicitingReceivedPN + = largestAckElicitingSentPN = largestSentAckedPN = largestAckedPNReceivedByPeer = -1L; + this.debugStrSupplier = debugStrSupplier; + this.debug = Utils.getDebugLogger(debugStrSupplier); + this.instantSource = instantSource; + this.rttEstimator = rttEstimator; + this.congestionController = congestionController; + this.packetNumberSpace = packetNumberSpace; + this.packetEmitter = packetEmitter; + this.emittedAckTracker = new EmittedAckTracker(); + this.packetTransmissionTask = new PacketTransmissionTask(); + this.quicTLSEngine = quicTLSEngine; + maxAckDelay = (packetNumberSpace == PacketNumberSpace.APPLICATION) + ? MAX_ACK_DELAY : 0; + } + + /** + * {@return the max delay before emitting a non ACK-eliciting packet to + * acknowledge a received ACK-eliciting packet, in milliseconds} + */ + public long getMaxAckDelay() { + return maxAckDelay; + } + + /** + * {@return the max ACK delay of the peer, in milliseconds} + */ + public long getPeerMaxAckDelayMillis() { + return peerMaxAckDelayMillis; + } + + /** + * Changes the value of the {@linkplain #getPeerMaxAckDelayMillis() + * peer max ACK delay} and ack delay exponent + * + * @param peerDelay the new delay, in milliseconds + * @param ackDelayExponent the new ack delay exponent + */ + @Override + public void updatePeerTransportParameters(long peerDelay, long ackDelayExponent) { + this.peerAckDelayExponent = ackDelayExponent; + this.peerMaxAckDelayMillis = peerDelay; + } + + @Override + public PacketNumberSpace packetNumberSpace() { + return packetNumberSpace; + } + + @Override + public long allocateNextPN() { + return nextPN.getAndIncrement(); + } + + @Override + public long getLargestPeerAckedPN() { + return largestReceivedAckedPN; + } + + @Override + public long getLargestProcessedPN() { + return largestProcessedPN; + } + + @Override + public long getMinPNThreshold() { + return largestAckedPNReceivedByPeer; + } + + @Override + public long getLargestSentAckedPN() { + return largestSentAckedPN; + } + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful negotiation of a new version. + * In that case we should stop retransmitting packet that have the + * "wrong" version: they will never be acknowledged. + */ + public void versionChanged() { + // don't retransmit packet with "bad" version + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (debug.on()) { + debug.log("version changed - clearing pending acks"); + } + clearAll(); + } + + public void retry() { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (debug.on()) { + debug.log("retry received - clearing pending acks"); + } + clearAll(); + } + + @Override + public ReentrantLock getTransmitLock() { + return transmitLock; + } + + // adds the PingRequest to the pendingPingRequests queue so + // that it can be completed when the packet is ACK'ed. + private void registerPingRequest(PingRequest pingRequest) { + if (closed) { + pingRequest.response().completeExceptionally(new IOException("closed")); + return; + } + pendingPingRequests.add(pingRequest); + // could be acknowledged already! + processPingResponses(largestReceivedAckedPN); + } + + @Override + public void close() { + if (closed) { + return; + } + synchronized (this) { + if (closed) return; + closed = true; + } + if (Log.quicControl() || Log.quicRetransmit()) { + Log.logQuic("{0} closing packet space {1}", + packetEmitter.logTag(), packetNumberSpace); + } + if (debug.on()) { + debug.log("closing packet space"); + } + // stop the internal scheduler + packetTransmissionTask.handleScheduler.stop(); + // make sure the task gets eventually removed from the timer + packetEmitter.reschedule(packetTransmissionTask); + // clear pending acks, retransmissions + transferLock.lock(); + try { + clearAll(); + // discard the (TLS) keys + if (debug.on()) { + debug.log("discarding TLS keys"); + } + this.quicTLSEngine.discardKeys(tlsEncryptionLevel()); + } finally { + transferLock.unlock(); + } + rttEstimator.resetPtoBackoff(); + // complete any ping request that hasn't been completed + IOException io = null; + try { + for (var pr : pendingPingRequests) { + if (io == null) { + io = new IOException("Not sending ping because " + + this.packetNumberSpace + " packet space is being closed"); + } + // TODO: is it necessary for this to be an exceptional completion? + pr.response().completeExceptionally(io); + } + } finally { + pendingPingRequests.clear(); + } + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void runTransmitter() { + transmitNow = true; + // run the handle loop + packetTransmissionTask.handle(); + } + + @Override + public void packetReceived(PacketType packet, long packetNumber, boolean isAckEliciting) { + assert PacketNumberSpace.of(packet) == packetNumberSpace; + assert packetNumber > largestAckedPNReceivedByPeer; + + if (closed) { + if (debug.on()) { + debug.log("%s closed, ignoring %s(pn: %s)", packetNumberSpace, packet, packetNumber); + } + return; + } + + if (debug.on()) { + debug.log("packetReceived %s(pn:%d, needsAck:%s)", + packet, packetNumber, isAckEliciting); + } + + // whether the packet is ack eliciting or not, we need to add its packet + // number to the ack frame. + packetProcessed(packetNumber); + addToAckFrame(packetNumber, isAckEliciting); + } + + // used in tests + public T triggeredForRetransmission(Function walker) { + return walker.apply(triggeredForRetransmission.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + public T pendingRetransmission(Function walker) { + return walker.apply(pendingRetransmission.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + // used in tests + public T pendingAcknowledgements(Function walker) { + return walker.apply(pendingAcknowledgements.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + // used in tests + public AtomicLong getNextPN() { return nextPN; } + + // Called by the retransmitLoop scheduler. + // Retransmit one packet for which retransmission has been triggered by + // the PacketTransmissionTask. + // return true if something was retransmitted, or false if there was nothing to retransmit + private boolean retransmit() throws QuicKeyUnavailableException, QuicTransportException { + PendingAcknowledgement pending; + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("already closed; retransmission dropped"); + } + clearAll(); + return false; + } + transferLock.lock(); + try { + pending = triggeredForRetransmission.poll(); + } finally { + transferLock.unlock(); + } + + if (pending != null) { + // allocate new packet number + // create new packet + // encrypt packet + // send packet + if (debug.on()) debug.log("handle: retransmitting..."); + packetEmitter.retransmit(this, pending.packet(), pending.attempts()); + return true; + } + return false; + } + + /** + * Called by the {@link PacketTransmissionTask} to + * generate a non ACK eliciting packet containing only the given + * ACK frame. + * + *

    If a received packet is ACK-eliciting, then it will be either + * directly acknowledged by {@link QuicConnectionImpl} - which will + * call {@link #getNextAckFrame(boolean)} to embed the {@link AckFrame} + * in a packet, or by a non-eliciting ACK packet which will be + * triggered {@link #getMaxAckDelay() maxAckDelay} after the reception + * of the ACK-eliciting packet (this method, triggered by the {@link + * PacketTransmissionTask}). + * + *

    This method doesn't reset the {@linkplain #getNextAckFrame(boolean) + * next ack frame} to be sent, but reset its delay so that only + * one non ACK-eliciting packet is emitted to acknowledge a given + * packet. + * + * @param ackFrame The ACK frame to send. + * @return the packet number of the emitted packet + */ + private long emitAckPacket(AckFrame ackFrame, boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + final boolean closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("Packet space closed, ack/ping won't be sent" + + (ackFrame != null ? ": " + ackFrame : "")); + } + return -1L; + } + try { + return packetEmitter.emitAckPacket(this, ackFrame, sendPing); + } catch (QuicKeyUnavailableException | QuicTransportException e) { + if (!this.isOpenForTransmission()) { + // possible race condition where the packet space was closed (and keys discarded) + // while there was an attempt to send an ACK/PING frame. + // Ignore such cases, since it's OK to not send those frames when the packet space + // is already closed + if (debug.on()) { + debug.log("ack/ping wasn't sent since packet space was closed" + + (ackFrame != null ? ": " + ackFrame : "")); + } + return -1L; + } + throw e; + } + } + + boolean isClosing(QuicPacket packet) { + var frames = packet.frames(); + if (frames == null || frames.isEmpty()) return false; + return frames.stream() + .anyMatch(ConnectionCloseFrame.class::isInstance); + } + + private synchronized void lastAckElicitingSent(long packetNumber) { + if (largestAckElicitingSentPN < packetNumber) { + largestAckElicitingSentPN = packetNumber; + } + } + + @Override + public void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber) { + if (packetNumber < 0) { + throw new IllegalArgumentException("Invalid packet number: " + packetNumber); + } + largestAckSent(AckFrame.largestAcknowledgedInPacket(packet)); + if (previousPacketNumber >= 0) { + if (debug.on()) { + debug.log("retransmitted packet %s(%d) as %d", + packet.packetType(), previousPacketNumber, packetNumber); + } + + boolean found = false; + transferLock.lock(); + try { + // check for close and addAcknowledgement in the same lock + // to avoid races with close / clearAll + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("%s already closed: ignoring packet pn:%s", + packetNumberSpace, packet.packetNumber()); + } + return; + } + // TODO: should use a tail set here to skip all pending acks + // whose packet number is < previousPacketNumber? + var iterator = pendingRetransmission.iterator(); + PendingAcknowledgement replacement; + while (iterator.hasNext()) { + PendingAcknowledgement pending = iterator.next(); + if (pending.hasPreviousNumber(previousPacketNumber)) { + // no need to retransmit twice, but can this happen? + iterator.remove(); + } else if (!found && pending.hasExactNumber(previousPacketNumber)) { + PreviousNumbers previous = new PreviousNumbers( + previousPacketNumber, + pending.largestAcknowledged, pending.previousNumbers); + replacement = + new PendingAcknowledgement(packet, now(), packetNumber, previous); + if (debug.on()) { + debug.log("Packet %s(pn:%s) previous %s(pn:%s) is pending acknowledgement", + packet.packetType(), packetNumber, packet.packetType(), previousPacketNumber); + } + var rep = replacement; + if (lostPackets.removeIf(p -> rep.hasPreviousNumber(p.packetNumber))) { + lostPackets.add(rep); + } + addAcknowledgement(replacement); + iterator.remove(); + found = true; + } + } + } finally { + transferLock.unlock(); + } + if (found && packetTransmissionTask.hasNoDeadline()) { + packetTransmissionTask.reschedule(); + } + if (!found) { + if (debug.on()) { + debug.log("packetRetransmitted: packet not found - previous: %s for %s(%s)", + previousPacketNumber, packet.packetType(), packetNumber); + } + } + } else { + if (packet.isAckEliciting()) { + // This method works with the following assumption: + // - Non ACK eliciting packet do not need to be retransmitted because: + // - they only contain ack frames - which may/will we be retransmitted + // anyway with the next ack eliciting packet + // - they will not be acknowledged directly - we don't want to + // resend them constantly + if (debug.on()) { + debug.log("Packet %s(pn:%s) is pending acknowledgement", + packet.packetType(), packetNumber); + } + PendingAcknowledgement pending = new PendingAcknowledgement(packet, + now(), packetNumber, null); + transferLock.lock(); + try { + // check for close and addAcknowledgement in the same lock + // to avoid races with close / clearAll + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("%s already closed: ignoring packet pn:%s", + packetNumberSpace, packet.packetNumber()); + } + return; + } + addAcknowledgement(pending); + if (packetTransmissionTask.hasNoDeadline()) { + packetTransmissionTask.reschedule(); + } + } finally { + transferLock.unlock(); + } + } + } + + + } + + private void addAcknowledgement(PendingAcknowledgement ack) { + lastAckElicitingSent(ack.sent); + lastAckElicitingSent(ack.packetNumber); + pendingAcknowledgements.add(ack); + sendTimes.put(ack.packetNumber, ack.sent); + congestionController.packetSent(ack.packet().size()); + } + + /** + * Computes the next deadline for generating a non ACK eliciting + * packet containing the next ACK frame, or for retransmitting + * unacknowledged packets for which retransmission is due. + * This may be different to the {@link #nextScheduledDeadline()} + * if newer changes have not been taken into account yet. + * @return the deadline at which the scheduler's task for this packet + * space should be scheduled to wake up + */ + public Deadline computeNextDeadline() { + return computeNextDeadline(true); + } + + public Deadline computeNextDeadline(boolean verbose) { + + if (closed) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] closed - no deadline", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MAX; + } + if (transmitNow) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] transmit now", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MIN; + } + if (pingRequested != null) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] ping requested", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MIN; + } + var ack = nextAckFrame; + + Deadline ackDeadline = (ack == null || ack.sent() != null) + ? Deadline.MAX // if the ack frame has already been sent, getNextAck() returns null + : ack.deadline(); + Deadline lossDeadline = getLossTimer(); + // TODO: consider removing the debug traces in this method when integrating + // if both loss deadline and PTO timer are set, loss deadline is always earlier + if (verbose && debug.on() && lossDeadline != Deadline.MIN) debug.log("lossDeadline is: " + lossDeadline); + if (lossDeadline != null) { + if (verbose && debug.on()) { + if (lossDeadline == Deadline.MIN) { + debug.log("lossDeadline is immediate"); + } else if (!ackDeadline.isBefore(lossDeadline)) { + debug.log("lossDeadline in %s ms", + Deadline.between(now(), lossDeadline).toMillis()); + } else { + debug.log("ackDeadline before lossDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] loss deadline: %s, ackDeadline: %s, deadline in %s", + packetEmitter.logTag(), packetNumberSpace, lossDeadline, ackDeadline, + Utils.debugDeadline(now(), min(ackDeadline, lossDeadline)))); + } + return min(ackDeadline, lossDeadline); + } + Deadline ptoDeadline = getPtoDeadline(); + if (verbose && debug.on()) debug.log("ptoDeadline is: " + ptoDeadline); + if (ptoDeadline != null) { + if (verbose && debug.on()) { + if (!ackDeadline.isBefore(ptoDeadline)) { + debug.log("ptoDeadline in %s ms", + Deadline.between(now(), ptoDeadline).toMillis()); + } else { + debug.log("ackDeadline before ptoDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] PTO deadline: %s, ackDeadline: %s, deadline in %s", + packetEmitter.logTag(), packetNumberSpace, ptoDeadline, ackDeadline, + Utils.debugDeadline(now(), min(ackDeadline, ptoDeadline)))); + } + return min(ackDeadline, ptoDeadline); + } + if (verbose && debug.on()) { + if (ackDeadline == Deadline.MAX) { + debug.log("ackDeadline is: Deadline.MAX"); + } else { + debug.log("ackDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (ackDeadline.equals(Deadline.MAX)) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] no deadline: " + + "pendingAcks: %s, triggered: %s, pendingRetransmit: %s", + packetEmitter.logTag(), packetNumberSpace, pendingAcknowledgements.size(), + triggeredForRetransmission.size(), pendingRetransmission.size())); + } + } else { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] deadline is %s", + packetEmitter.logTag(), packetNumberSpace(), + Utils.debugDeadline(now(), ackDeadline))); + } + } + return ackDeadline; + } + + /** + * {@return the next deadline at which the scheduler's task for this packet + * space is currently scheduled to wake up} + */ + public Deadline nextScheduledDeadline() { + return packetTransmissionTask.nextDeadline; + } + + private Deadline now() { + return instantSource.instant(); + } + + /** + * Tracks the largest packet acknowledged by the packets acknowledged in the + * given AckFrame. This helps to implement the algorithm described in + * RFC 9000, 13.2.4. Limiting Ranges by Tracking ACK Frames. + * @param pending a yet unacknowledged packet that may be acknowledged + * by the given{@link AckFrame}. + * @param frame a received {@code AckFrame} + * @return whether the given pending unacknowledged packet is being + * acknowledged by this ack frame. + */ + private boolean trackAcknowlegment(PendingAcknowledgement pending, AckFrame frame) { + return emittedAckTracker.trackAcknowlegment(pending, frame); + } + + private boolean isAcknowledgingLostPacket(PendingAcknowledgement pending, AckFrame frame, + List[] recovered) { + if (frame.isAcknowledging(pending.packetNumber)) { + if (recovered != null) { + if (recovered[0] == null) { + recovered[0] = new ArrayList<>(); + } + recovered[0].add(pending); + } + return true; + } + // There is a potential for a never ending retransmission + // loop here if we don't treat the ack of a previous packet just + // as the ack of the tip of the chain. + // So we call packetEmitter.acknowledged(pending.packet()) here too, + // and return `true` in this case as well. + var previous = pending.findPreviousAcknowledged(frame); + if (previous != null) { + if (recovered != null) { + if (recovered[0] == null) { + recovered[0] = new ArrayList<>(); + } + recovered[0].add(pending); + } + return true; + } + return false; + + } + + @Override + public void processAckFrame(AckFrame frame) throws QuicTransportException { + // for each acknowledged packet, remove it from the + // list of packets pending acknowledgement, or from the + // list of packets pending retransmission + long largestAckAckedBefore = emittedAckTracker.largestAckAcked(); + long largestAcknowledged = frame.largestAcknowledged(); + Deadline now = now(); + if (largestAcknowledged >= nextPN.get()) { + throw new QuicTransportException("Acknowledgement for a nonexistent packet", + null, frame.getTypeField(), QuicTransportErrors.PROTOCOL_VIOLATION); + } + + int lostCount; + transferLock.lock(); + try { + if (largestAckReceived(largestAcknowledged)) { + // if the largest acknowledged PN is newly acknowledged + // and at least one of the newly acked packets is ack-eliciting + // -> use the new RTT sample + // the below code only checks if largest acknowledged is ack-eliciting + Deadline sentTime = sendTimes.get(largestAcknowledged); + if (sentTime != null) { + long ackDelayMicros; + if (isApplicationSpace()) { + confirmHandshake(); + long baseAckDelay = peerAckDelayToMicros(frame.ackDelay()); + // if packet was sent after handshake confirmed, use max ack delay + if (largestAcknowledged >= handshakeConfirmedPN) { + ackDelayMicros = Math.min( + baseAckDelay, + TimeUnit.MILLISECONDS.toMicros(peerMaxAckDelayMillis)); + } else { + ackDelayMicros = baseAckDelay; + } + } else { + // acks are not delayed during handshake + ackDelayMicros = 0; + } + long rttSample = sentTime.until(now, ChronoUnit.MICROS); + if (debug.on()) { + debug.log("New RTT sample on packet %s: %s us (delay %s us)", + largestAcknowledged, rttSample, + ackDelayMicros); + } + rttEstimator.consumeRttSample( + rttSample, + ackDelayMicros, + now + ); + } else { + if (debug.on()) { + debug.log("RTT sample on packet %s ignored: not ack eliciting", + largestAcknowledged); + } + } + if (packetNumberSpace != PacketNumberSpace.INITIAL) { + rttEstimator.resetPtoBackoff(); + } + purgeSendTimes(largestAcknowledged); + // complete PingRequests if needed + processPingResponses(largestAcknowledged); + } else { + if (debug.on()) { + debug.log("RTT sample on packet %s ignored: not largest", + largestAcknowledged); + } + } + + pendingRetransmission.removeIf((p) -> trackAcknowlegment(p, frame)); + triggeredForRetransmission.removeIf((p) -> trackAcknowlegment(p, frame)); + for (Iterator iterator = pendingAcknowledgements.iterator(); iterator.hasNext(); ) { + PendingAcknowledgement p = iterator.next(); + if (trackAcknowlegment(p, frame)) { + iterator.remove(); + congestionController.packetAcked(p.packet.size(), p.sent); + } + } + lostCount = detectAndRemoveLostPackets(now); + @SuppressWarnings({"unchecked","rawtypes"}) + List[] recovered= Log.quicRetransmit() ? new List[1] : null; + lostPackets.removeIf((p) -> isAcknowledgingLostPacket(p, frame, recovered)); + if (recovered != null && recovered[0] != null) { + Log.logQuic("{0} lost packets recovered: {1}({2}) total unrecovered {3}, unacknowledged {4}", + packetEmitter.logTag(), packetType(), + recovered[0].stream().map(PendingAcknowledgement::packetNumber).toList(), + lostPackets.size(), pendingAcknowledgements.size() + pendingRetransmission.size()); + } + } finally { + transferLock.unlock(); + } + + long largestAckAcked = emittedAckTracker.largestAckAcked(); + if (largestAckAcked > largestAckAckedBefore) { + if (debug.on()) { + debug.log("%s: largestAckAcked=%d - cleaning up AckFrame", + packetNumberSpace, largestAckAcked); + } + // remove ack ranges that we no longer need to acknowledge. + // this implements the algorithm described in RFC 9000, + // 13.2.4. Limiting Ranges by Tracking ACK Frames + cleanupAcks(); + } + + if (lostCount > 0) { + if (debug.on()) + debug.log("Found %s lost packets", lostCount); + // retransmit if possible + runTransmitter(); + } else if (blockedByCC && congestionController.canSendPacket()) { + // CC just got unblocked... send more data + blockedByCC = false; + runTransmitter(); + } else { + // RTT was updated, some packets might be lost, recompute timers + packetTransmissionTask.reschedule(); + } + } + + @Override + public void confirmHandshake() { + assert isApplicationSpace(); + if (handshakeConfirmedPN == 0) { + handshakeConfirmedPN = nextPN.get(); + } + } + + private void purgeSendTimes(long largestAcknowledged) { + sendTimes.headMap(largestAcknowledged, true).clear(); + } + + private long peerAckDelayToMicros(long ackDelay) { + return ackDelay << peerAckDelayExponent; + } + + private NextAckFrame getNextAck(boolean onlyOverdue, int maxSize) { + Deadline now = now(); + // This method is called to retrieve the AckFrame that will + // be embedded in the next packet sent to the peer. + // We therefore need to disarm the timer that will send a + // non-ACK eliciting packet with that AckFrame (if any) before + // returning the AckFrame. This is the purpose of the loop + // below... + while (true) { + NextAckFrame ack = nextAckFrame; + if (ack == null + || ack.deadline() == Deadline.MAX + || (onlyOverdue && ack.deadline().isAfter(now)) + || ack.sent() != null) { + return null; + } + // also reserve 3 bytes for the ack delay + if (ack.ackFrame().size() > maxSize - 3) return null; + NextAckFrame newAck = ack.withDeadline(Deadline.MAX, now); + boolean respin = !Handles.NEXTACK.compareAndSet(this, ack, newAck); + if (!respin) { + return ack; + } + } + } + + @Override + public AckFrame getNextAckFrame(boolean onlyOverdue) { + return getNextAckFrame(onlyOverdue, Integer.MAX_VALUE); + } + + @Override + public AckFrame getNextAckFrame(boolean onlyOverdue, int maxSize) { + if (closed) { + return null; + } + NextAckFrame ack = getNextAck(onlyOverdue, maxSize); + if (ack == null) { + return null; + } + long delay = ack.lastUpdated() + .until(now(), ChronoUnit.MICROS) >> ACK_DELAY_EXPONENT; + return ack.ackFrame().withAckDelay(delay); + } + + /** + * Returns the count of unacknowledged packets that were declared lost. + * The lost packets are moved from the pendingAcknowledgements + * into the pendingRetransmission. + * + * @param now current time, used for time-based loss detection. + */ + private int detectAndRemoveLostPackets(Deadline now) { + Deadline lossSendTime = now.minus(rttEstimator.getLossThreshold()); + int count = 0; + // debug.log("preparing for retransmission"); + transferLock.lock(); + try { + List lost = Log.quicRetransmit() ? new ArrayList<>() : null; + List packets = new ArrayList<>(); + Deadline firstSendTime = null, lastSendTime = null; + for (PendingAcknowledgement head = pendingAcknowledgements.peek(); + head != null && head.packetNumber < largestReceivedAckedPN; + head = pendingAcknowledgements.peek()) { + if (head.packetNumber < largestReceivedAckedPN - kPacketThreshold || + !lossSendTime.isBefore(head.sent)) { + if (debug.on()) { + debug.log("retransmit:head pn:" + head.packetNumber + + ",largest acked PN:" + largestReceivedAckedPN + + ",sent:" + head.sent + + ",lossSendTime:" + lossSendTime + ); + } + if (pendingAcknowledgements.remove(head)) { + pendingRetransmission.add(head); + triggeredForRetransmission.add(head); + packets.add(head.packet); + if (firstSendTime == null) { + firstSendTime = head.sent; + } + lastSendTime = head.sent; + var lp = head; + lostPackets.removeIf(p -> lp.hasPreviousNumber(p.packetNumber)); + lostPackets.add(head); + count++; + if (lost != null) lost.add(head); + } + } else { + if (debug.on()) { + debug.log("no retransmit:head pn:" + head.packetNumber + + ",largest acked PN:" + largestReceivedAckedPN + + ",sent:" + head.sent + + ",lossSendTime:" + lossSendTime + ); + } + break; + } + } + if (!packets.isEmpty()) { + // Persistent congestion is detected more aggressively than mandated by RFC 9002: + // - may be reported even if there's no prior RTT sample + // - may be reported even if there are acknowledged packets between the lost ones + boolean persistent = Deadline.between(firstSendTime, lastSendTime) + .compareTo(getPersistentCongestionDuration()) > 0; + congestionController.packetLost(packets, lastSendTime, persistent); + } + if (lost != null && !lost.isEmpty()) { + Log.logQuic("{0} lost packet {1}({2}) total unrecovered {3}, unacknowledged {4}", + packetEmitter.logTag(), + packetType(), lost.stream().map(PendingAcknowledgement::packetNumber).toList(), + lostPackets.size(), pendingAcknowledgements.size() + pendingRetransmission.size()); + } + } finally { + transferLock.unlock(); + } + return count; + } + + PacketType packetType() { + return switch (packetNumberSpace) { + case INITIAL -> PacketType.INITIAL; + case HANDSHAKE -> PacketType.HANDSHAKE; + case APPLICATION -> PacketType.ONERTT; + case NONE -> PacketType.NONE; + }; + } + + /** + * {@return true if PTO timer expired, false otherwise} + */ + private boolean isPTO(Deadline now) { + Deadline ptoDeadline = getPtoDeadline(); + return ptoDeadline != null && !ptoDeadline.isAfter(now); + } + + // returns true if this space is the APPLICATION space + private boolean isApplicationSpace() { + return packetNumberSpace == PacketNumberSpace.APPLICATION; + } + + // returns the PTO duration + Duration getPtoDuration() { + var pto = rttEstimator.getBasePtoDuration() + .plusMillis(peerMaxAckDelayMillis) + .multipliedBy(rttEstimator.getPtoBackoff()); + var max = QuicRttEstimator.MAX_PTO_BACKOFF_TIMEOUT; + // don't allow PTO > 240s + return pto.compareTo(max) > 0 ? max : pto; + } + + // returns the persistent congestion duration + Duration getPersistentCongestionDuration() { + return rttEstimator.getBasePtoDuration() + .plusMillis(peerMaxAckDelayMillis) + .multipliedBy(kPersistentCongestionThreshold); + } + + private Deadline getPtoDeadline() { + if (packetNumberSpace == PacketNumberSpace.INITIAL && lastAckElicitingTime != null) { + if (!quicTLSEngine.keysAvailable(QuicTLSEngine.KeySpace.HANDSHAKE)) { + // if handshake keys are not available, initial PTO must be set + return lastAckElicitingTime.plus(getPtoDuration()); + } + } + if (packetNumberSpace == PacketNumberSpace.HANDSHAKE) { + // set anti-deadlock timer + if (lastAckElicitingTime == null) { + lastAckElicitingTime = now(); + } + if (largestAckElicitingSentPN == -1) { + return lastAckElicitingTime.plus(getPtoDuration()); + } + } + if (largestAckElicitingSentPN <= largestReceivedAckedPN) { + return null; + } + // Application space deadline can only be set when handshake is confirmed + if (isApplicationSpace() && quicTLSEngine.getHandshakeState() != QuicTLSEngine.HandshakeState.HANDSHAKE_CONFIRMED) { + return null; + } + return lastAckElicitingTime.plus(getPtoDuration()); + } + + private Deadline getLossTimer() { + PendingAcknowledgement head = pendingAcknowledgements.peek(); + if (head == null || head.packetNumber >= largestReceivedAckedPN) { + return null; + } + if (head.packetNumber < largestReceivedAckedPN - kPacketThreshold) { + return Deadline.MIN; + } + return head.sent.plus(rttEstimator.getLossThreshold()); + } + + // Compute the new deadline when adding an ack-eliciting packet number + // to an ack frame which is not empty. + private Deadline computeNewDeadlineFor(AckFrame frame, Deadline now, Deadline deadline, + long packetNumber, long previousLargest, + long ackDelay) { + + boolean previousEliciting = !deadline.equals(Deadline.MAX); + + if (closed) return Deadline.MAX; + + if (previousEliciting) { + // RFC 9000 #13.2.2: + // We should send an ACK immediately after receiving two + // ACK-eliciting packets + if (debug.on()) { + debug.log("two ACK-Eliciting packets received: " + + "next ack deadline now"); + } + return now; + } else if (packetNumber < previousLargest) { + // RFC 9000 #13.2.1: + // if the packet has PN less than another ack-eliciting packet, + // send ACK frame as soon as possible + if (debug.on()) { + debug.log("ACK-Eliciting packet received out of order: " + + "next ack deadline now"); + } + return now; + } else if (packetNumber - 1 > previousLargest && previousLargest > -1) { + // RFC 9000 #13.2.1: + // Check whether there are gaps between this packet and the + // previous ACK-eliciting packet that was received: + // if we find any gap we should send an ACK frame as soon + // as possible + if (!frame.isRangeAcknowledged(previousLargest + 1, packetNumber)) { + if (debug.on()) { + debug.log("gaps detected between this packet" + + " and the previous ACK eliciting packet: " + + "next ack deadline now"); + } + return now; + } + } + // send ACK within max delay + return now.plusMillis(ackDelay); + } + + /** + * Used to request sending of a ping frame, for instance, to verify that + * the connection is alive. + * @return a completable future that will be completed with the time it + * took, in milliseconds, for the peer to acknowledge the packet that + * contained the PingFrame (or any packet that was sent after) + */ + @Override + public CompletableFuture requestSendPing() { + CompletableFuture pingRequested; + synchronized (this) { + if ((pingRequested = this.pingRequested) == null) { + pingRequested = this.pingRequested = new MinimalFuture<>(); + } + } + runTransmitter(); + return pingRequested; + } + + // Look at whether a ping frame should be sent with the + // next ACK frame... + // If a PING frame should be sent, return the new deadline (now) + // Otherwise, return Deadline.MAX; + // A PING frame will be sent if: + // - the AckFrame contains more than (10) ACK Ranges + // - and no ACK eliciting packet was sent, or the last ACK-eliciting was + // sent long enough ago - typically 1 PTO delay + // These numbers are implementation dependent and not defined in the RFC, but + // help implement a strategy that sends occasional PING frames to limit the size + // of the ACK frames - as described in RFC 9000. + // + // See RFC 9000 Section 13.2.4 + private boolean shouldSendPing(Deadline now, AckFrame frame) { + Deadline last = lastAckElicitingTime; + if (frame != null && + (last == null || + last.isBefore(now.minus(rttEstimator.getBasePtoDuration()))) + && frame.ackRanges().size() > MAX_ACKRANGE_COUNT_BEFORE_PING) { + return true; + } + return false; + } + + // TODO: store the builder instead of storing the AckFrame? + // storing a builder would mean locking - so it might not be a good + // idea. But creating a new builder and AckFrame each time means + // producing more garbage for the GC to collect. + // This method is called when a new packet is received, and it adds the + // received packet number to the next ACK frame to send out. + // If the packet is ACK eliciting it also arms a timeout (if needed) + // to make sure the packet will be acknowledged within the committed + // time frame. + private void addToAckFrame(long packetNumber, boolean isAckEliciting) { + + long largestAckEliciting = largestAckElicitingReceivedPN; + if (isAckEliciting) ackElicitingPacketProcessed(packetNumber); + + if (debug.on()) { + if (packetNumber < largestAckEliciting) { + debug.log("already received a larger ACK eliciting packet"); + } + } + + // compute a new AckFrame that includes the + // provided packet number + NextAckFrame nextAckFrame, ack = null; + boolean reschedule; + long largestAckAcked; + long newLargestAckAcked = -1; + do { + Deadline now = now(); + nextAckFrame = this.nextAckFrame; + var frame = nextAckFrame == null ? null : nextAckFrame.ackFrame(); + largestAckAcked = emittedAckTracker.largestAckAcked(); + boolean needNewFrame = (frame == null || !frame.isAcknowledging(packetNumber)) + && packetNumber > largestAckAcked; + if (needNewFrame) { + if (debug.on()) { + debug.log("Adding %s(%d) to ackFrame %s (ackEliciting %s)", + packetNumberSpace, packetNumber, nextAckFrame, isAckEliciting); + } + var builder = AckFrameBuilder + .ofNullable(frame) + .dropAcksBefore(largestAckAcked) + .addAck(packetNumber); + assert !builder.isEmpty(); + frame = builder.build(); + + // Note: we could optimize this if needed by simply using a max number of + // ranges: we could pre-compute the approximate size of a frame that has N ranges + // and use that. + final int maxFrameSize = QuicConnectionImpl.SMALLEST_MAXIMUM_DATAGRAM_SIZE - 100; + if (frame.size() > maxFrameSize) { + // frame is too big. We will drop some ranges + int ranges = frame.ackRanges().size(); + int index = ranges/3; + builder.dropAckRangesAfter(index); + newLargestAckAcked = builder.getLargestAckAcked(); + var newFrame = builder.build(); + if (Log.quicCC() || Log.quicRetransmit()) { + Log.logQuic("{0}: frame too big ({1} bytes) dropping ack ranges after {2}, " + + "will ignore packets smaller than {3} (new frame: {4} bytes)", + debugStrSupplier.get(), Integer.toString(frame.size()), + Integer.toString(index), Long.toString(newLargestAckAcked), + Integer.toString(newFrame.size())); + } + frame = newFrame; + assert frame.size() <= maxFrameSize; + } + assert frame.isAcknowledging(packetNumber); + if (nextAckFrame == null) { + if (debug.on()) debug.log("no previous ackframe"); + Deadline deadline = isAckEliciting + ? now.plusMillis(maxAckDelay) + : Deadline.MAX; + ack = new NextAckFrame(frame, deadline, now, null); + reschedule = isAckEliciting; + if (debug.on()) debug.log("next deadline: " + maxAckDelay); + } else { + Deadline deadline = nextAckFrame.deadline(); + Deadline nextDeadline = deadline; + boolean deadlineNotExpired = now.isBefore(deadline); + if (isAckEliciting && deadlineNotExpired) { + if (debug.on()) debug.log("computing new deadline for ackframe"); + nextDeadline = computeNewDeadlineFor(frame, now, deadline, + packetNumber, largestAckEliciting, maxAckDelay); + } + long millisToNext = nextDeadline.equals(Deadline.MAX) + ? Long.MAX_VALUE + : now.until(nextDeadline, ChronoUnit.MILLIS); + if (debug.on()) { + if (nextDeadline == Deadline.MAX) { + debug.log("next deadline is: Deadline.MAX"); + } else { + debug.log("next deadline is: " + millisToNext); + } + } + ack = new NextAckFrame(frame, nextDeadline, now, null); + reschedule = !nextDeadline.equals(deadline) + || millisToNext <= 0; + } + if (debug.on()) { + String delay = reschedule ? Utils.millis(now(), ack.deadline()) + : "not rescheduled"; + debug.log("%s: new ackFrame composed: %s - reschedule=%s", + packetNumberSpace, ack.ackFrame(), delay); + } + } else { + reschedule = false; + if (debug.on()) { + debug.log("packet %s(%d) is already in ackFrame %s", + packetNumberSpace, packetNumber, nextAckFrame); + } + break; + } + } while (!Handles.NEXTACK.compareAndSet(this, nextAckFrame, ack)); + + if (newLargestAckAcked >= 0) { + // we reduced the frame because it was too big: we need to ignore + // packets that are larger than the new largest ignored packet. + // this is now our new de-facto 'largestAckAcked' even if it wasn't + // really acked by the peer + emittedAckTracker.dropPacketNumbersSmallerThan(newLargestAckAcked); + } + + var ackFrame = ack == null ? null : ack.ackFrame(); + assert packetNumber <= largestAckAcked + || ackFrame != null && ackFrame.isAcknowledging(packetNumber) + || nextAckFrame != null && nextAckFrame.ackFrame() != null + && nextAckFrame.ackFrame.isAcknowledging(packetNumber) + : "packet %s(%s) should be in ackFrame" + .formatted(packetNumberSpace, packetNumber); + + if (reschedule) { + runTransmitter(); + } + } + + void debugState() { + if (debug.on()) { + debug.log("state: %s", isClosed() ? "closed" : "opened" ); + debug.log("AckFrame: " + nextAckFrame); + String pendingAcks = pendingAcknowledgements.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + String pendingRetransmit = pendingRetransmission.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + debug.log("Pending acks: %s", pendingAcks); + debug.log("Pending retransmit: %s", pendingRetransmit); + } + } + + void debugState(String prefix, StringBuilder sb) { + String state = isClosed() ? "closed" : "opened"; + sb.append(prefix).append("State: ").append(state).append('\n'); + sb.append(prefix).append("AckFrame: ").append(nextAckFrame).append('\n'); + String pendingAcks = pendingAcknowledgements.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + String pendingRetransmit = pendingRetransmission.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + sb.append(prefix).append("Pending acks: ").append(pendingAcks).append('\n'); + sb.append(prefix).append("Pending retransmit: ").append(pendingRetransmit); + } + + @Override + public boolean isAcknowledged(long packetNumber) { + var ack = nextAckFrame; + var ackFrame = ack == null ? null : ack.ackFrame(); + var largestProcessed = largestProcessedPN; + // if ackFrame is null it means all packets <= largestProcessedPN + // have been acked. + if (ackFrame == null) return packetNumber <= largestProcessed; + if (packetNumber > largestProcessed) return false; + var largestAckedPNReceivedByPeer = this.largestAckedPNReceivedByPeer; + if (packetNumber <= largestAckedPNReceivedByPeer) return true; + return ackFrame.isAcknowledging(packetNumber); + } + + @Override + public void fastRetransmit() { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (closed || fastRetransmitDone) { + return; + } + fastRetransmit = true; + if (Log.quicControl() || Log.quicRetransmit()) { + Log.logQuic("Scheduling fast retransmit"); + } else if (debug.on()) { + debug.log("Scheduling fast retransmit"); + } + runTransmitter(); + + } + + private static Deadline min(Deadline one, Deadline two) { + return two.isAfter(one) ? one : two; + } + + // This implements the algorithm described in RFC 9000: + // 13.2.4. Limiting Ranges by Tracking ACK Frames + private void cleanupAcks() { + // clean up the next ACK frame, removing all packets <= largestAckAcked + NextAckFrame nextAckFrame, ack = null; + long largestAckAcked; + do { + nextAckFrame = this.nextAckFrame; + if (nextAckFrame == null) return; // nothing to do! + var frame = nextAckFrame.ackFrame(); + largestAckAcked = emittedAckTracker.largestAckAcked(); + boolean needNewFrame = frame != null + && frame.smallestAcknowledged() <= largestAckAcked; + if (needNewFrame) { + if (debug.on()) { + debug.log("Dropping all acks below %s(%d) in ackFrame %s", + packetNumberSpace, largestAckAcked, nextAckFrame); + } + var builder = AckFrameBuilder + .ofNullable(frame) + .dropAcksBefore(largestAckAcked); + frame = builder.isEmpty() ? null : builder.build(); + if (frame == null) { + ack = null; + if (debug.on()) { + debug.log("%s: ackFrame cleared - nothing to acknowledge", + packetNumberSpace); + } + } else { + Deadline deadline = nextAckFrame.deadline(); + ack = new NextAckFrame(frame, deadline, + nextAckFrame.lastUpdated(), nextAckFrame.sent()); + if (debug.on()) { + debug.log("%s: ackFrame cleaned up: %s", + packetNumberSpace, ack.ackFrame()); + } + } + } else { + if (debug.on()) { + debug.log("%s: no packet smaller than %d in ackFrame %s", + packetNumberSpace, largestAckAcked, nextAckFrame); + } + break; + } + } while (!Handles.NEXTACK.compareAndSet(this, nextAckFrame, ack)); + + var ackFrame = ack == null ? null : ack.ackFrame(); + assert ackFrame == null || ackFrame.smallestAcknowledged() > largestAckAcked + : "%s(pn > %s) should not acknowledge packet <= %s" + .formatted(packetNumberSpace, ackFrame.smallestAcknowledged(), largestAckAcked); + } + + private long ackElicitingPacketProcessed(long packetNumber) { + long largestPN; + do { + largestPN = largestAckElicitingReceivedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_ACK_ELICITING_RECEIVED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + private long packetProcessed(long packetNumber) { + long largestPN; + do { + largestPN = largestProcessedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_PROCESSED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + /** + * Theoretically we should wait for the packet that contains the + * ping frame to be acknowledged, but if we receive the ack of a + * packet with a larger number, we can assume that the connection + * is still alive, and therefore complete the ping response. + * @param packetNumber the acknowledged packet number + */ + private void processPingResponses(long packetNumber) { + if (pendingPingRequests.isEmpty()) return; + var iterator = pendingPingRequests.iterator(); + while (iterator.hasNext()) { + var pr = iterator.next(); + if (pr.packetNumber() <= packetNumber) { + iterator.remove(); + pr.response().complete(pr.sent().until(now(), ChronoUnit.MILLIS)); + } else { + // this is a queue, so the PingRequest with the smaller + // packet number will be at the head. We can stop iterating + // as soon as we find a PingRequest that has a packet + // number larger than the one acknowledged. + break; + } + } + } + + private long largestAckSent(long packetNumber) { + long largestPN; + do { + largestPN = largestSentAckedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_SENT_ACKED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + private boolean largestAckReceived(long packetNumber) { + long largestPN; + do { + largestPN = largestReceivedAckedPN; + if (largestPN >= packetNumber) return false; // already up to date + } while (!Handles.LARGEST_RECEIVED_ACKED_PN + .compareAndSet(this, largestPN, packetNumber)); + return true; // updated + } + + // records the time at which the last ACK-eliciting packet was sent. + // This has the side effect of resetting the nextPingTime to Deadline.MAX + // The logic is that a PING frame only need to be sent if no ACK-eliciting + // packet has been sent for some time (and the AckFrame has grown big enough). + // See RFC 9000 - Section 13.2.4 + private Deadline lastAckElicitingSent(Deadline now) { + Deadline max; + if (debug.on()) + debug.log("Updating last send time to %s", now); + do { + max = lastAckElicitingTime; + if (max != null && !now.isAfter(max)) return max; + } while (!Handles.LAST_ACK_ELICITING_TIME + .compareAndSet(this, max, now)); + return now; + } + + /** + * returns the TLS encryption level of this packet space as specified + * in RFC-9001, section 4, table 1. + */ + private QuicTLSEngine.KeySpace tlsEncryptionLevel() { + return switch (this.packetNumberSpace) { + case INITIAL -> QuicTLSEngine.KeySpace.INITIAL; + // APPLICATION packet space could even mean 0-RTT, but currently we don't support 0-RTT + case APPLICATION -> QuicTLSEngine.KeySpace.ONE_RTT; + case HANDSHAKE -> QuicTLSEngine.KeySpace.HANDSHAKE; + default -> throw new IllegalStateException("No known TLS encryption level" + + " for packet space: " + this.packetNumberSpace); + }; + } + + // VarHandle provide the same atomic compareAndSet functionality + // than AtomicXXXXX classes, but without the additional cost in + // footprint. + private static final class Handles { + private Handles() {throw new InternalError();} + static final VarHandle DEADLINE; + static final VarHandle NEXTACK; + static final VarHandle LARGEST_PROCESSED_PN; + static final VarHandle LARGEST_ACK_ELICITING_RECEIVED_PN; + static final VarHandle LARGEST_RECEIVED_ACKED_PN; + static final VarHandle LARGEST_SENT_ACKED_PN; + static final VarHandle LARGEST_ACK_ACKED_PN; + static final VarHandle LAST_ACK_ELICITING_TIME; + static final VarHandle IGNORE_ALL_PN_BEFORE; + static { + Lookup lookup = MethodHandles.lookup(); + try { + Class srt = PacketTransmissionTask.class; + DEADLINE = lookup.findVarHandle(srt, "nextDeadline", Deadline.class); + + Class pmc = PacketSpaceManager.class; + LAST_ACK_ELICITING_TIME = lookup.findVarHandle(pmc, + "lastAckElicitingTime", Deadline.class); + NEXTACK = lookup.findVarHandle(pmc, "nextAckFrame", NextAckFrame.class); + LARGEST_RECEIVED_ACKED_PN = lookup + .findVarHandle(pmc, "largestReceivedAckedPN", long.class); + LARGEST_SENT_ACKED_PN = lookup + .findVarHandle(pmc, "largestSentAckedPN", long.class); + LARGEST_PROCESSED_PN = lookup + .findVarHandle(pmc, "largestProcessedPN", long.class); + LARGEST_ACK_ELICITING_RECEIVED_PN = lookup + .findVarHandle(pmc, "largestAckElicitingReceivedPN", long.class); + LARGEST_ACK_ACKED_PN = lookup + .findVarHandle(pmc, "largestAckedPNReceivedByPeer", long.class); + + Class eat = EmittedAckTracker.class; + IGNORE_ALL_PN_BEFORE = lookup + .findVarHandle(eat, "ignoreAllPacketsBefore", long.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + + } + } + } + + static final class OneRttPacketSpaceManager extends PacketSpaceManager + implements QuicOneRttContext { + + OneRttPacketSpaceManager(final QuicConnectionImpl connection) { + super(connection, PacketNumberSpace.APPLICATION); + } + } + + static final class HandshakePacketSpaceManager extends PacketSpaceManager { + private final PacketSpaceManager initialPktSpaceMgr; + private final boolean isClientConnection; + private final AtomicBoolean firstPktSent = new AtomicBoolean(); + + HandshakePacketSpaceManager(final QuicConnectionImpl connection, + final PacketSpaceManager initialPktSpaceManager) { + super(connection, PacketNumberSpace.HANDSHAKE); + this.isClientConnection = connection.isClientConnection(); + this.initialPktSpaceMgr = initialPktSpaceManager; + } + + @Override + public void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber) { + super.packetSent(packet, previousPacketNumber, packetNumber); + if (!isClientConnection) { + // nothing additional to be done for server connections + return; + } + if (firstPktSent.compareAndSet(false, true)) { + // if this is the first packet we sent in the HANDSHAKE keyspace + // then we close the INITIAL space discard the INITIAL keys. + // RFC-9000, section 17.2.2.1: + // A client stops both sending and processing Initial packets when it sends + // its first Handshake packet. ... Though packets might still be in flight or + // awaiting acknowledgment, no further Initial packets need to be exchanged + // beyond this point. Initial packet protection keys are discarded along with + // any loss recovery and congestion control state + if (debug.on()) { + debug.log("first handshake packet sent by client, initiating close of" + + " INITIAL packet space"); + } + this.initialPktSpaceMgr.close(); + } + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java new file mode 100644 index 00000000000..065d045b57c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * Manages the connection ids advertised by a peer of a connection. + * - Handles incoming NEW_CONNECTION_ID frames, + * - produces outgoing RETIRE_CONNECTION_ID frames, + * - registers received stateless reset tokens with the QuicEndpoint + * Additionally on the client side: + * - handles incoming transport parameters (preferred_address, stateless_reset_token) + * - stores original and retry peer IDs + */ +// TODO implement voluntary switching of connection IDs +final class PeerConnIdManager { + private final Logger debug; + private final QuicConnectionImpl connection; + private final String logTag; + private final boolean isClient; + + private enum State { + INITIAL_PKT_NOT_RECEIVED_FROM_PEER, + RETRY_PKT_RECEIVED_FROM_PEER, + PEER_CONN_ID_FINALIZED + } + + private volatile State state = State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER; + + private QuicConnectionId clientSelectedDestConnId; + private QuicConnectionId peerDecidedRetryConnId; + // sequence number of active connection ID + private long activeConnIdSeq = -1; + private QuicConnectionId activeConnId; + + // the connection ids (there can be more than one) with which the peer identifies this connection. + // the key of this Map is a (RFC defined) sequence number for the connection id + private final NavigableMap peerConnectionIds = + Collections.synchronizedNavigableMap(new TreeMap<>()); + // the connection id sequence numbers that we haven't received yet. + // We need to know which sequence numbers are retired, and which are not assigned yet + private final NavigableSet gaps = + Collections.synchronizedNavigableSet(new TreeSet<>()); + // the connection id sequence numbers that are awaiting retirement. + private final Queue toRetire = new ArrayDeque<>(); + // the largest retirePriorTo value received across NEW_CONNECTION_ID frames + private volatile long largestReceivedRetirePriorTo = -1; // -1 implies none received so far + // the largest sequenceNumber value received across NEW_CONNECTION_ID frames + private volatile long largestReceivedSequenceNumber; + private final ReentrantLock lock = new ReentrantLock(); + + PeerConnIdManager(final QuicConnectionImpl connection, final String dbTag) { + this.isClient = connection.isClientConnection(); + this.debug = Utils.getDebugLogger(() -> dbTag); + this.logTag = connection.logTag(); + this.connection = connection; + } + + /** + * Save the client-selected original server connection ID + * + * @param peerConnId the client-selected original server connection ID + */ + void originalServerConnId(final QuicConnectionId peerConnId) { + lock.lock(); + try { + final var st = this.state; + if (st != State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER) { + throw new IllegalStateException("Cannot associate a client selected peer id" + + " in current state " + st); + } + this.clientSelectedDestConnId = peerConnId; + this.activeConnId = peerConnId; + } finally { + lock.unlock(); + } + } + + /** + * {@return the client-selected original server connection ID} + */ + QuicConnectionId originalServerConnId() { + lock.lock(); + try { + final var id = this.clientSelectedDestConnId; + if (id == null) { + throw new IllegalArgumentException("Original (peer) connection id not yet set"); + } + return id; + } finally { + lock.unlock(); + } + } + + /** + * Save the server-selected retry connection ID + * + * @param peerConnId the server-selected retry connection ID + */ + void retryConnId(final QuicConnectionId peerConnId) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final var st = this.state; + if (st != State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER) { + throw new IllegalStateException("Cannot associate a peer id, from retry packet," + + " in current state " + st); + } + this.peerDecidedRetryConnId = peerConnId; + this.activeConnId = peerConnId; + this.state = State.RETRY_PKT_RECEIVED_FROM_PEER; + } finally { + lock.unlock(); + } + } + + /** + * Returns the connectionId the server included in the Source Connection ID field of a + * Retry packet. May be null. + * + * @return the connection id sent in the server's retry packet + */ + QuicConnectionId retryConnId() { + lock.lock(); + try { + return this.peerDecidedRetryConnId; + } finally { + lock.unlock(); + } + } + + /** + * The peer in its INITIAL packet would have sent a connection id representing itself. That + * connection id may not be the same that we might have sent in the INITIAL packet. If it isn't + * the same, then we switch the peer connection id, that we keep track off, to the one that + * the peer has chosen. + * + * @param initialPacket the INITIAL packet from the peer + */ + void finalizeHandshakePeerConnId(final InitialPacket initialPacket) throws QuicTransportException { + lock.lock(); + try { + final QuicConnectionId sourceId = initialPacket.sourceId(); + final var st = this.state; + if (st == State.PEER_CONN_ID_FINALIZED) { + // we have already finalized the peer connection id, through a previous INITIAL + // packet receipt (there can be more than one INITIAL packets). + // now we just verify that this INITIAL packet too has the finalized peer connection + // id and if it doesn't then we throw an exception + final QuicConnectionId handshakePeerConnId = this.peerConnectionIds.get(0L); + assert handshakePeerConnId != null : "Handshake peer connection id is unavailable"; + if (!handshakePeerConnId.equals(sourceId)) { + throw new QuicTransportException("Invalid source connection id in INITIAL packet", + QuicTLSEngine.KeySpace.INITIAL, 0, PROTOCOL_VIOLATION); + } + return; + } + // this is the first INITIAL packet from the peer, so we finalize the peer connection id + final PeerConnectionId handshakePeerConnId = new PeerConnectionId(sourceId.getBytes()); + // at this point we have either switched to a new peer connection id (chosen by the peer) + // or have agreed to use the one we chose for the peer. In either case, we register this + // as the handshake peer connection id with sequence number 0. + // RFC-9000, section 5.1.1: The initial connection ID issued by an endpoint is sent in + // the Source Connection ID field of the long packet header during the handshake. + // The sequence number of the initial connection ID is 0. + this.peerConnectionIds.put(0L, handshakePeerConnId); + this.state = State.PEER_CONN_ID_FINALIZED; + this.activeConnIdSeq = 0; + this.activeConnId = handshakePeerConnId; + if (debug.on()) { + debug.log("scid: %s finalized handshake peerConnectionId as: %s", + connection.localConnectionId().toHexString(), + handshakePeerConnId.toHexString()); + } + } finally { + lock.unlock(); + } + } + + /** + * Save the connection ID from the preferred address QUIC transport parameter + * + * @param preferredConnId preferred connection ID + * @param preferredStatelessResetToken preferred stateless reset token + */ + void handlePreferredAddress(final ByteBuffer preferredConnId, + final byte[] preferredStatelessResetToken) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final PeerConnectionId peerConnId = new PeerConnectionId(preferredConnId, + preferredStatelessResetToken); + // keep track of this peer connection id + // RFC-9000, section 5.1.1: If the preferred_address transport parameter is sent, + // the sequence number of the supplied connection ID is 1 + assert largestReceivedSequenceNumber == 0; + this.peerConnectionIds.put(1L, peerConnId); + largestReceivedSequenceNumber = 1; + } finally { + lock.unlock(); + } + } + + /** + * Save the stateless reset token QUIC transport parameter + * + * @param statelessResetToken stateless reset token + */ + void handshakeStatelessResetToken(final byte[] statelessResetToken) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final QuicConnectionId handshakeConnId = this.peerConnectionIds.get(0L); + if (handshakeConnId == null) { + throw new IllegalStateException("No handshake peer connection available"); + } + // recreate the conn id with the stateless token + this.peerConnectionIds.put(0L, new PeerConnectionId(handshakeConnId.asReadOnlyBuffer(), + statelessResetToken)); + // register with the endpoint + connection.endpoint().associateStatelessResetToken(statelessResetToken, connection); + } finally { + lock.unlock(); + } + } + + /** + * {@return the active peer connection ID} + */ + QuicConnectionId getPeerConnId() { + lock.lock(); + try { + if (activeConnIdSeq < largestReceivedRetirePriorTo) { + // stop using the old connection ID + switchConnectionId(); + } + return activeConnId; + } finally { + lock.unlock(); + } + } + + private QuicConnectionId getPeerConnId(final long sequenceNum) { + assert lock.isHeldByCurrentThread(); + return this.peerConnectionIds.get(sequenceNum); + } + + /** + * Process the incoming NEW_CONNECTION_ID frame. + * + * @param newCid the NEW_CONNECTION_ID frame + * @throws QuicTransportException if the frame violates the protocol + */ + void handleNewConnectionIdFrame(final NewConnectionIDFrame newCid) + throws QuicTransportException { + if (debug.on()) { + debug.log("Received NEW_CONNECTION_ID frame: %s", newCid); + } + // pre-checks + final long sequenceNumber = newCid.sequenceNumber(); + assert sequenceNumber >= 0 : "negative sequence number disallowed in new connection id frame"; + final long retirePriorTo = newCid.retirePriorTo(); + if (retirePriorTo > sequenceNumber) { + // RFC 9000, section 19.15: Receiving a value in the Retire Prior To field that is greater + // than that in the Sequence Number field MUST be treated as a connection error of + // type FRAME_ENCODING_ERROR + throw new QuicTransportException("Invalid retirePriorTo " + retirePriorTo, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final ByteBuffer connectionId = newCid.connectionId(); + final int connIdLength = connectionId.remaining(); + if (connIdLength < 1 || connIdLength > MAX_CONNECTION_ID_LENGTH) { + // RFC-9000, section 19.15: Values less than 1 and greater than 20 are invalid and + // MUST be treated as a connection error of type FRAME_ENCODING_ERROR + throw new QuicTransportException("Invalid connection id length " + connIdLength, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final ByteBuffer statelessResetToken = newCid.statelessResetToken(); + assert statelessResetToken.remaining() == QuicConnectionImpl.RESET_TOKEN_LENGTH; + lock.lock(); + try { + // see if we have received any connection ids for this same sequence number. + // this is possible if the packet containing the new connection id frame was retransmitted. + // the connection id for such a (duplicate) sequence number is expected to be the same. + // RFC-9000, section 19.15: if a sequence number is used for different connection IDs, + // the endpoint MAY treat that receipt as a connection error of type PROTOCOL_VIOLATION + final QuicConnectionId previousConnIdForSeqNum = getPeerConnId(sequenceNumber); + if (previousConnIdForSeqNum != null) { + if (previousConnIdForSeqNum.matches(connectionId)) { + // frame with same sequence number and connection id, probably a retransmission. + // ignore this frame + if (Log.trace()) { + Log.logTrace("{0} Ignoring (duplicate) new connection id frame with" + + " sequence number {1}", logTag, sequenceNumber); + } + if (debug.on()) { + debug.log("Ignoring (duplicate) new connection id frame with" + + " sequence number %d", sequenceNumber); + } + return; + } + // mismatch, throw protocol violation error + throw new QuicTransportException("Invalid connection id in (duplicated)" + + " new connection id frame with sequence number " + sequenceNumber, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), PROTOCOL_VIOLATION); + } + if ((sequenceNumber <= largestReceivedSequenceNumber && !gaps.contains(sequenceNumber)) + || sequenceNumber < largestReceivedRetirePriorTo) { + if (Log.trace()) { + Log.logTrace("{0} Ignoring (retired) new connection id frame with" + + " sequence number {1}", logTag, sequenceNumber); + } + if (debug.on()) { + debug.log("Ignoring (retired) new connection id frame with" + + " sequence number %d", sequenceNumber); + } + return; + } + long numConnIdsToAdd = Math.max(sequenceNumber - largestReceivedSequenceNumber, 0); + final long numCurrentActivePeerConnIds = this.peerConnectionIds.size() + this.gaps.size(); + // we can temporarily store up to 3x the active connection ID limit, + // including active and retired IDs. + if (numCurrentActivePeerConnIds + numConnIdsToAdd + toRetire.size() + > 3 * this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.1: After processing a NEW_CONNECTION_ID frame and adding and + // retiring active connection IDs, if the number of active connection IDs exceeds + // the value advertised in its active_connection_id_limit transport parameter, + // an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR + throw new QuicTransportException("Connection id limit reached", + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + // end pre-checks + // if we reached here, the number of connection IDs is less than twice the active limit. + // Insert gaps for the sequence numbers we haven't seen yet + insertGaps(sequenceNumber); + // Update the list of sequence numbers to retire + retirePriorTo(retirePriorTo); + // insert the new connection ID + final byte[] statelessResetTokenBytes = new byte[QuicConnectionImpl.RESET_TOKEN_LENGTH]; + statelessResetToken.get(statelessResetTokenBytes); + final PeerConnectionId newPeerConnId = new PeerConnectionId(connectionId, statelessResetTokenBytes); + final var previous = this.peerConnectionIds.putIfAbsent(sequenceNumber, newPeerConnId); + assert previous == null : "A peer connection id already exists for sequence number " + + sequenceNumber; + // post-checks + // now we can accurately check the number of active and retired connection IDs + if (peerConnectionIds.size() + gaps.size() + > this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.1: After processing a NEW_CONNECTION_ID frame and adding and + // retiring active connection IDs, if the number of active connection IDs exceeds + // the value advertised in its active_connection_id_limit transport parameter, + // an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR + throw new QuicTransportException("Active connection id limit reached", + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + if (toRetire.size() > 2 * this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.2: + // An endpoint SHOULD limit the number of connection IDs it has retired locally for + // which RETIRE_CONNECTION_ID frames have not yet been acknowledged. + // An endpoint SHOULD allow for sending and tracking a number + // of RETIRE_CONNECTION_ID frames of at least twice the value + // of the active_connection_id_limit transport parameter + throw new QuicTransportException("Retired connection id limit reached: " + toRetire, + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + if (this.largestReceivedRetirePriorTo < retirePriorTo) { + this.largestReceivedRetirePriorTo = retirePriorTo; + } + if (this.largestReceivedSequenceNumber < sequenceNumber) { + this.largestReceivedSequenceNumber = sequenceNumber; + } + } finally { + lock.unlock(); + } + } + + private void switchConnectionId() { + assert lock.isHeldByCurrentThread(); + // the caller is expected to retire the active connection id prior to calling this + assert !peerConnectionIds.containsKey(activeConnIdSeq); + Map.Entry entry = peerConnectionIds.ceilingEntry(largestReceivedRetirePriorTo); + activeConnIdSeq = entry.getKey(); + activeConnId = entry.getValue(); + // link the peer issued stateless reset token to this connection + final QuicEndpoint endpoint = this.connection.endpoint(); + endpoint.associateStatelessResetToken(entry.getValue().getStatelessResetToken(), this.connection); + + if (Log.trace()) { + Log.logTrace("{0} Switching to connection ID {1}", logTag, activeConnIdSeq); + } + if (debug.on()) { + debug.log("Switching to connection ID %d", activeConnIdSeq); + } + } + + private void insertGaps(long sequenceNumber) { + assert lock.isHeldByCurrentThread(); + for (long i = largestReceivedSequenceNumber + 1; i < sequenceNumber; i++) { + gaps.add(i); + } + } + + private void retirePriorTo(final long priorTo) { + assert lock.isHeldByCurrentThread(); + // remove/retire (in preparation of sending a RETIRE_CONNECTION_ID frames) + for (Iterator> iterator = peerConnectionIds.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + final long seqNumToRetire = entry.getKey(); + if (seqNumToRetire >= priorTo) { + break; + } + iterator.remove(); + toRetire.add(seqNumToRetire); + // Note that the QuicEndpoint only stores local connection ids and doesn't store peer + // connection ids. It does however store the peer-issued stateless reset token of a + // peer connection id, so we let the endpoint know that the stateless reset token needs + // to be forgotten since the corresponding peer connection id is being retired + final byte[] resetTokenToForget = entry.getValue().getStatelessResetToken(); + if (resetTokenToForget != null) { + this.connection.endpoint().forgetStatelessResetToken(resetTokenToForget); + } + } + for (Iterator iterator = gaps.iterator(); iterator.hasNext(); ) { + Long gap = iterator.next(); + if (gap >= priorTo) { + return; + } + iterator.remove(); + toRetire.add(gap); + } + } + + /** + * Produce a queued RETIRE_CONNECTION_ID frame, if it fits in the packet + * + * @param remaining bytes remaining in the packet + * @return a RetireConnectionIdFrame, or null if none is queued or remaining is too low + */ + public QuicFrame nextFrame(int remaining) { + // retire connection id: + // type - 1 byte + // sequence number - var int + if (remaining < 9) { + return null; + } + lock.lock(); + try { + final Long seqNumToRetire = toRetire.poll(); + if (seqNumToRetire != null) { + if (seqNumToRetire == activeConnIdSeq) { + // can't send this connection ID yet, we will send it in the next packet + toRetire.add(seqNumToRetire); + return null; + } + return new RetireConnectionIDFrame(seqNumToRetire); + } + return null; + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java new file mode 100644 index 00000000000..0c6f946d1a2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + + +/** + * A free-form connection ID to wrap the connection ID bytes + * sent by the peer. + * Client and server might impose some structure on the + * connection ID bytes. For instance, they might choose to + * encode the connection ID length in the connection ID bytes. + * This class makes no assumption on the structure of the + * connection id bytes. + */ +public final class PeerConnectionId extends QuicConnectionId { + private final byte[] statelessResetToken; + + /** + * A new {@link QuicConnectionId} represented by the given bytes. + * @param connId The connection ID bytes. + */ + public PeerConnectionId(final byte[] connId) { + super(ByteBuffer.wrap(connId.clone())); + this.statelessResetToken = null; + } + + /** + * A new {@link QuicConnectionId} represented by the given bytes. + * @param connId The connection ID bytes. + * @param statelessResetToken The stateless reset token to be associated with this connection id. + * Can be null. + * @throws IllegalArgumentException If the {@code statelessResetToken} is non-null and if its + * length isn't 16 bytes + * + */ + public PeerConnectionId(final ByteBuffer connId, final byte[] statelessResetToken) { + super(cloneBuffer(connId)); + if (statelessResetToken != null) { + if (statelessResetToken.length != 16) { + throw new IllegalArgumentException("Invalid stateless reset token length " + + statelessResetToken.length); + } + this.statelessResetToken = statelessResetToken.clone(); + } else { + this.statelessResetToken = null; + } + } + + private static ByteBuffer cloneBuffer(ByteBuffer src) { + final byte[] idBytes = new byte[src.remaining()]; + src.get(idBytes); + return ByteBuffer.wrap(idBytes); + } + + /** + * {@return the stateless reset token associated with this connection id. returns null if no + * token exists} + */ + public byte[] getStatelessResetToken() { + return this.statelessResetToken == null ? null : this.statelessResetToken.clone(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "(length:" + length() + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java new file mode 100644 index 00000000000..58a07c22f66 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.LongFunction; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.AltServicesRegistry; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint.QuicEndpointFactory; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +/** + * This class represents a QuicClient. + * The QuicClient is responsible for creating/returning instances + * of QuicConnection for a given AltService, and for linking them + * with an instance of QuicEndpoint and QuicSelector for reading + * and writing Datagrams off the network. + * A QuicClient is also a factory for QuicConnectionIds. + * There is a 1-1 relationship between a QuicClient and an Http3Client. + * A QuicClient can be closed: closing a QuicClient will close all + * quic connections opened on that client. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public final class QuicClient implements QuicInstance, AutoCloseable { + private static final AtomicLong IDS = new AtomicLong(); + private static final AtomicLong CONNECTIONS = new AtomicLong(); + + private final Logger debug = Utils.getDebugLogger(this::name); + + // See RFC 9000 section 14 + static final int SMALLEST_MAXIMUM_DATAGRAM_SIZE = 1200; + static final int INITIAL_SERVER_CONNECTION_ID_LENGTH = 17; + static final int MAX_ENDPOINTS_LIMIT = 16; + static final int DEFAULT_MAX_ENDPOINTS = Utils.getIntegerNetProperty( + "jdk.httpclient.quic.maxEndpoints", 1); + + private final String clientId; + private final String name; + private final Executor executor; + private final QuicTLSContext quicTLSContext; + private final SSLParameters sslParameters; + // QUIC versions in their descending order of preference + private final List availableVersions; + private final InetSocketAddress bindAddress; + private final QuicTransportParameters transportParams; + private final ReentrantLock lock = new ReentrantLock(); + private final QuicEndpoint[] endpoints = new QuicEndpoint[computeMaxEndpoints()]; + private int insertionPoint; + private volatile QuicSelector selector; + private volatile boolean closed; + // keep track of any initial tokens that a server has advertised for use. The key in this + // map is the server's host and port representation and the value is the token to use. + private final Map initialTokens = new ConcurrentHashMap<>(); + private final QuicEndpointFactory endpointFactory = new QuicEndpointFactory(); + private final LongFunction appErrorCodeToString; + + private QuicClient(final QuicClient.Builder builder) { + Objects.requireNonNull(builder, "Quic client builder"); + if (builder.availableVersions == null) { + throw new IllegalStateException("Need at least one available Quic version"); + } + if (builder.tlsContext == null) { + throw new IllegalStateException("No QuicTLSContext set"); + } + this.clientId = builder.clientId == null ? nextName() : builder.clientId; + this.name = "QuicClient(%s)".formatted(clientId); + this.appErrorCodeToString = builder.appErrorCodeToString == null + ? QuicInstance.super::appErrorToString + : builder.appErrorCodeToString; + // verify that QUIC TLS supports all requested QUIC versions + var test = new ArrayList<>(builder.availableVersions); + test.removeAll(builder.tlsContext.createEngine().getSupportedQuicVersions()); + if (!test.isEmpty()) { + throw new IllegalArgumentException( + "Requested QUIC versions not supported by TLS: " + test); + } + this.availableVersions = builder.availableVersions; + this.quicTLSContext = builder.tlsContext; + this.bindAddress = builder.bindAddr == null ? new InetSocketAddress(0) : builder.bindAddr; + this.executor = builder.executor; + this.sslParameters = builder.sslParams == null + ? new SSLParameters() + : requireTLS13(builder.sslParams); + this.transportParams = builder.transportParams; + if (debug.on()) debug.log("created"); + } + + + private static int computeMaxEndpoints() { + // available processors may change according to the API doc, + // so recompute this for each new client... + int availableProcessors = Runtime.getRuntime().availableProcessors(); + int max = DEFAULT_MAX_ENDPOINTS <= 0 ? availableProcessors >> 1 : DEFAULT_MAX_ENDPOINTS; + return Math.clamp(max, 1, MAX_ENDPOINTS_LIMIT); + } + + // verifies that the TLS protocol(s) configured in SSLParameters, if any, + // allows TLSv1.3 + private static SSLParameters requireTLS13(final SSLParameters parameters) { + final String[] protos = parameters.getProtocols(); + if (protos == null || protos.length == 0) { + // no specific protocols specified, so it's OK + return parameters; + } + for (final String proto : protos) { + if ("TLSv1.3".equals(proto)) { + // TLSv1.3 is allowed, that's good + return parameters; + } + } + // explicit TLS protocols have been configured in SSLParameters and it doesn't + // include TLSv1.3. QUIC mandates TLSv1.3, so we can't use this SSLParameters + throw new IllegalArgumentException("TLSv1.3 is required for QUIC," + + " but SSLParameters is configured with " + Arrays.toString(protos)); + } + + @Override + public String appErrorToString(long code) { + return appErrorCodeToString.apply(code); + } + + @Override + public QuicTransportParameters getTransportParameters() { + if (this.transportParams == null) { + return null; + } + // return a copy + return new QuicTransportParameters(this.transportParams); + } + + private static String nextName() { + return "quic-client-" + IDS.incrementAndGet(); + } + + /** + * The address that the QuicEndpoint will bind to. + * @implNote By default, this is wildcard:0 + * @return the address that the QuicEndpoint will bind to. + */ + public InetSocketAddress bindAddress() { + return bindAddress; + } + + @Override + public boolean isVersionAvailable(final QuicVersion quicVersion) { + return this.availableVersions.contains(quicVersion); + } + + /** + * {@return the versions that are available for use on this instance, in the descending order + * of their preference} + */ + @Override + public List getAvailableVersions() { + return this.availableVersions; + } + + /** + * Creates a new unconnected {@code QuicConnection} to the given + * {@code service}. + * + * @param service the alternate service for which to create the connection for + * @return a new unconnected {@code QuicConnection} + * @throws IllegalArgumentException if the ALPN of this transport isn't the same as that of the + * passed alternate service + * @apiNote The caller is expected to call {@link QuicConnectionImpl#startHandshake()} to + * initiate the handshaking. The connection is considered "connected" when + * the handshake is successfully completed. + */ + public QuicConnectionImpl createConnectionFor(final AltServicesRegistry.AltService service) { + final InetSocketAddress peerAddress = new InetSocketAddress(service.identity().host(), + service.identity().port()); + final String alpn = service.alpn(); + if (alpn == null) { + throw new IllegalArgumentException("missing ALPN on alt service"); + } + final SSLParameters sslParameters = createSSLParameters(new String[]{alpn}); + return new QuicConnectionImpl(null, this, peerAddress, + service.origin().host(), service.origin().port(), sslParameters, "QuicClientConnection(%s)", + CONNECTIONS.incrementAndGet()); + } + + /** + * Creates a new unconnected {@code QuicConnection} to the given + * {@code peerAddress}. + * + * @param peerAddress the address of the peer + * @return a new unconnected {@code QuicConnection} + * @apiNote The caller is expected to call {@link QuicConnectionImpl#startHandshake()} to + * initiate the handshaking. The connection is considered "connected" when + * the handshake is successfully completed. + */ + public QuicConnectionImpl createConnectionFor(final InetSocketAddress peerAddress, + final String[] alpns) { + Objects.requireNonNull(peerAddress); + Objects.requireNonNull(alpns); + if (alpns.length == 0) { + throw new IllegalArgumentException("at least one ALPN is needed"); + } + final SSLParameters sslParameters = createSSLParameters(alpns); + return new QuicConnectionImpl(null, this, peerAddress, peerAddress.getHostString(), + peerAddress.getPort(), sslParameters, "QuicClientConnection(%s)", CONNECTIONS.incrementAndGet()); + } + + private SSLParameters createSSLParameters(final String[] alpns) { + final SSLParameters sslParameters = Utils.copySSLParameters(this.getSSLParameters()); + sslParameters.setApplicationProtocols(alpns); + // section 4.2, RFC-9001 (QUIC) Clients MUST NOT offer TLS versions older than 1.3 + sslParameters.setProtocols(new String[] {"TLSv1.3"}); + return sslParameters; + } + + @Override + public String instanceId() { + return clientId; + } + + @Override + public QuicTLSContext getQuicTLSContext() { + return quicTLSContext; + } + + @Override + public SSLParameters getSSLParameters() { + return Utils.copySSLParameters(sslParameters); + } + + /** + * The name identifying this QuicClient, used in debug traces. + * @implNote This is {@code "QuicClient()"}. + * @return the name identifying this QuicClient. + */ + public String name() { + return name; + } + + /** + * The HttpClientImpl Id. used to identify the client in + * debug traces. + * @return A string identifying the HttpClientImpl instance. + */ + public String clientId() { + return clientId; + } + + /** + * The executor used by this QuicClient when a task needs to + * be offloaded to a separate thread. + * @implNote This is the HttpClientImpl internal executor. + * @return the executor used by this QuicClient. + */ + @Override + public Executor executor() { + return executor; + } + + @Override + public QuicEndpoint getEndpoint() throws IOException { + return chooseEndpoint(); + } + + private QuicEndpoint chooseEndpoint() throws IOException { + QuicEndpoint endpoint; + lock.lock(); + try { + if (closed) throw new IllegalStateException("QuicClient is closed"); + int index = insertionPoint; + if (index >= endpoints.length) index = 0; + endpoint = endpoints[index]; + if (endpoint != null) { + if (endpoints.length == 1) return endpoint; + if (endpoint.connectionCount() < 2) return endpoint; + for (int i = 1; i < endpoints.length - 1; i++) { + var nexti = (index + i) % endpoints.length; + var next = endpoints[nexti]; + if (next == null) continue; + if (next.connectionCount() < endpoint.connectionCount()) { + endpoint = next; + index = nexti; + } + } + if (++index >= endpoints.length) index = 0; + insertionPoint = index; + + if (Log.quicControl()) { + Log.logQuic("Selecting endpoint: " + endpoint.name()); + } else if (debug.on()) { + debug.log("Selecting endpoint: " + endpoint.name()); + } + + return endpoint; + } + + final var endpointName = "QuicEndpoint(" + clientId + "-" + index + ")"; + if (Log.quicControl()) { + Log.logQuic("Adding new endpoint: " + endpointName); + } else if (debug.on()) { + debug.log("Adding new endpoint: " + endpointName); + } + endpoint = createEndpoint(endpointName); + assert endpoints[index] == null; + endpoints[index] = endpoint; + insertionPoint = index + 1; + } finally { + lock.unlock(); + } + // register the newly created endpoint with the selector + QuicEndpoint.registerWithSelector(endpoint, selector, debug); + return endpoint; + } + + /** + * Creates an endpoint with the given name, and register it with a selector. + * @return the new QuicEndpoint + * @throws IOException if an error occurs when setting up the selector + * or linking the transport with the selector. + * @throws IllegalStateException if the client is closed. + */ + private QuicEndpoint createEndpoint(final String endpointName) throws IOException { + var selector = this.selector; + boolean newSelector = false; + final QuicEndpoint.ChannelType configuredChannelType = QuicEndpoint.CONFIGURED_CHANNEL_TYPE; + if (selector == null) { + // create a selector first + lock.lock(); + try { + if (closed) { + throw new IllegalStateException("QuicClient is closed"); + } + selector = this.selector; + if (selector == null) { + final String selectorName = "QuicSelector(" + clientId + ")"; + selector = this.selector = switch (configuredChannelType) { + case NON_BLOCKING_WITH_SELECTOR -> + QuicSelector.createQuicNioSelector(this, selectorName); + case BLOCKING_WITH_VIRTUAL_THREADS -> + QuicSelector.createQuicVirtualThreadPoller(this, selectorName); + }; + newSelector = true; + } + } finally { + lock.unlock(); + } + } + if (newSelector) { + // we may be closed when we reach here. It doesn't matter though. + // if the selector is closed before it's started the thread will + // immediately exit (or exit after the first wakeup) + selector.start(); + } + final QuicEndpoint endpoint = switch (configuredChannelType) { + case NON_BLOCKING_WITH_SELECTOR -> + endpointFactory.createSelectableEndpoint(this, endpointName, + bindAddress(), selector.timer()); + case BLOCKING_WITH_VIRTUAL_THREADS -> + endpointFactory.createVirtualThreadedEndpoint(this, endpointName, + bindAddress(), selector.timer()); + }; + assert endpoint.channelType() == configuredChannelType + : "bad endpoint for " + configuredChannelType + ": " + endpoint.getClass(); + return endpoint; + } + + @Override + public void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer) { + if (debug.on()) { + debug.log("dropping unmatched packet in buffer [%s, %d bytes, %s]", + type, buffer.remaining(), source); + } + } + + /** + * @param peerAddress The address of the server + * @return the initial token to use in INITIAL packets during connection establishment + * against a server represented by the {@code peerAddress}. Returns null if no token exists for + * the server. + */ + byte[] initialTokenFor(final InetSocketAddress peerAddress) { + if (peerAddress == null) { + return null; + } + final InitialTokenRecipient recipient = new InitialTokenRecipient(peerAddress.getHostString(), + peerAddress.getPort()); + // an initial token (obtained through NEW_TOKEN frame) can be used only once against the + // peer which advertised it. Hence, we remove it. + return this.initialTokens.remove(recipient); + } + + /** + * Registers a token to use in INITIAL packets during connection establishment against a server + * represented by the {@code peerAddress}. + * + * @param peerAddress The address of the server + * @param token The token to use + * @throws NullPointerException If either of {@code peerAddress} or {@code token} is null + * @throws IllegalArgumentException If the token is of zero length + */ + void registerInitialToken(final InetSocketAddress peerAddress, final byte[] token) { + Objects.requireNonNull(peerAddress); + Objects.requireNonNull(token); + if (token.length == 0) { + throw new IllegalArgumentException("Empty token"); + } + final InitialTokenRecipient recipient = new InitialTokenRecipient(peerAddress.getHostString(), + peerAddress.getPort()); + // multiple initial tokens (through NEW_TOKEN frame) can be sent by the same peer, but as + // per RFC-9000, section 8.1.3, it's OK for clients to just use the last received token, + // since the rest are less likely to be useful + this.initialTokens.put(recipient, token); + } + + @Override + public void close() { + // TODO: ignore exceptions while closing? + lock.lock(); + try { + if (closed) return; + closed = true; + } finally { + lock.unlock(); + } + for (int i = 0 ; i < endpoints.length ; i++) { + var endpoint = endpoints[i]; + if (endpoint != null) closeEndpoint(endpoint); + } + var selector = this.selector; + if (selector != null) selector.close(); + } + + private void closeEndpoint(QuicEndpoint endpoint) { + try { endpoint.close(); } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close endpoint: %s: %s", endpoint.name(), t); + } + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + lock.lock(); + try { + if (closed) return; + closed = true; + } finally { + lock.unlock(); + } + for (int i = 0 ; i < endpoints.length ; i++) { + var endpoint = endpoints[i]; + if (endpoint != null) abortEndpoint(endpoint, t); + } + var selector = this.selector; + if (selector != null) selector.abort(t); + } + + private void abortEndpoint(QuicEndpoint endpoint, Throwable cause) { + try { endpoint.abort(cause); } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to abort endpoint: %s: %s", endpoint.name(), t); + } + } + } + + private record InitialTokenRecipient (String host, int port) { + } + + public static final class Builder { + private String clientId; + private List availableVersions; + private Executor executor; + private SSLParameters sslParams; + private QuicTLSContext tlsContext; + private QuicTransportParameters transportParams; + private InetSocketAddress bindAddr; + private LongFunction appErrorCodeToString; + + public Builder availableVersions(final List versions) { + Objects.requireNonNull(versions, "Quic versions"); + if (versions.isEmpty()) { + throw new IllegalArgumentException("Need at least one available Quic version"); + } + this.availableVersions = List.copyOf(versions); + return this; + } + + public Builder applicationErrors(LongFunction errorCodeToString) { + this.appErrorCodeToString = errorCodeToString; + return this; + } + + public Builder availableVersions(final QuicVersion version, final QuicVersion... more) { + Objects.requireNonNull(version, "Quic version"); + if (more == null) { + this.availableVersions = List.of(version); + return this; + } + final List versions = new ArrayList<>(); + versions.add(version); + for (final QuicVersion v : more) { + Objects.requireNonNull(v, "Quic version"); + versions.add(v); + } + this.availableVersions = List.copyOf(versions); + return this; + } + + public Builder clientId(final String clientId) { + this.clientId = clientId; + return this; + } + + public Builder tlsContext(final QuicTLSContext tlsContext) { + this.tlsContext = tlsContext; + return this; + } + + public Builder sslParameters(final SSLParameters sslParameters) { + this.sslParams = sslParameters; + return this; + } + + public Builder bindAddress(final InetSocketAddress bindAddr) { + this.bindAddr = bindAddr; + return this; + } + + public Builder executor(final Executor executor) { + this.executor = executor; + return this; + } + + public Builder transportParameters(final QuicTransportParameters transportParams) { + this.transportParams = transportParams; + return this; + } + + public QuicClient build() { + return new QuicClient(this); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java new file mode 100644 index 00000000000..4bfad2c5560 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; + +public interface QuicCongestionController { + + /** + * {@return true if a new non-ACK packet can be sent at this time} + */ + boolean canSendPacket(); + + /** + * Update the maximum datagram size + * @param newSize new maximum datagram size. + */ + void updateMaxDatagramSize(int newSize); + + /** + * Update CC with a non-ACK packet + * @param packetBytes packet size in bytes + */ + void packetSent(int packetBytes); + + /** + * Update CC after a non-ACK packet is acked + * + * @param packetBytes acked packet size in bytes + * @param sentTime time when packet was sent + */ + void packetAcked(int packetBytes, Deadline sentTime); + + /** + * Update CC after packets are declared lost + * + * @param lostPackets collection of lost packets + * @param sentTime time when the most recent lost packet was sent + * @param persistent true if persistent congestion detected, false otherwise + */ + void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent); + + /** + * Update CC after packets are discarded + * @param discardedPackets collection of discarded packets + */ + void packetDiscarded(Collection discardedPackets); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java new file mode 100644 index 00000000000..05bfa1adc8c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreams; +import jdk.internal.net.quic.QuicTLSEngine; + +/** + * This class implements a QUIC connection. + * A QUIC connection is established between a client and a server + * over a QuicEndpoint endpoint. + * A QUIC connection can then multiplex multiple QUIC streams to the + * same server. + * This abstract class exposes public methods used by the higher level + * protocol. + * + *

    A typical call flow to establish a connection would be: + * {@snippet : + * AltService service = ...; + * QuicClient client = ...; + * QuicConnection connection = client.createConnectionFor(service); + * connection.startHandshake() + * .thenApply((r) -> { ... }) + * ...; + * } + * + */ +public abstract class QuicConnection { + + /** + * Starts the Quic Handshake. + * @return A completable future which will be completed when the + * handshake is completed. + * @throws UnsupportedOperationException If this connection isn't a client connection + */ + public abstract CompletableFuture startHandshake(); + + /** + * Creates a new locally initiated bidirectional stream. + *

    + * Creation of streams is limited to the maximum limit advertised by the peer. If a new stream + * cannot be created due to this limitation, then this method will use the + * {@code limitIncreaseDuration} to decide how long to wait for a potential increase in the + * limit. + *

    + * If the limit has been reached and the {@code limitIncreaseDuration} is not + * {@link Duration#isPositive() positive} then this method returns a {@code CompletableFuture} + * which has been completed exceptionally with {@link QuicStreamLimitException}. Else, this + * method returns a {@code CompletableFuture} which waits for that duration for a potential + * increase in the limit. If, during this period, the stream creation limit does increase and + * stream creation succeeds then the returned {@code CompletableFuture} will be completed + * successfully, else it will complete exceptionally with {@link QuicStreamLimitException}. + * + * @param limitIncreaseDuration Amount of time to wait for the bidirectional stream creation + * limit to be increased by the peer, if this connection has + * currently reached its limit + * @return a CompletableFuture which completes either with a new locally initiated + * bidirectional stream or exceptionally if the stream creation failed + */ + public abstract CompletableFuture openNewLocalBidiStream( + Duration limitIncreaseDuration); + + /** + * Creates a new locally initiated unidirectional stream. Locally created unidirectional streams + * are write-only streams. + *

    + * Creation of streams is limited to the maximum limit advertised by the peer. If a new stream + * cannot be created due to this limitation, then this method will use the + * {@code limitIncreaseDuration} to decide how long to wait for a potential increase in the + * limit. + *

    + * If the limit has been reached and the {@code limitIncreaseDuration} is not + * {@link Duration#isPositive() positive} then this method returns a {@code CompletableFuture} + * which has been completed exceptionally with {@link QuicStreamLimitException}. Else, this + * method returns a {@code CompletableFuture} which waits for that duration for a potential + * increase in the limit. If, during this period, the stream creation limit does increase and + * stream creation succeeds then the returned {@code CompletableFuture} will be completed + * successfully, else it will complete exceptionally with {@link QuicStreamLimitException}. + * + * @param limitIncreaseDuration Amount of time to wait for the unidirectional stream creation + * limit to be increased by the peer, if this connection has + * currently reached its limit + * @return a CompletableFuture which completes either with a new locally initiated + * unidirectional stream or exceptionally if the stream creation failed + */ + public abstract CompletableFuture openNewLocalUniStream( + Duration limitIncreaseDuration); + + /** + * Adds a listener that will be invoked when a remote stream is + * created. + * + * @apiNote + * The listener will be invoked with any remote streams + * already opened, and not yet acquired by another listener. + * Any stream passed to the listener is either a {@link QuicBidiStream} + * or a {@link QuicReceiverStream} depending on the + * {@linkplain QuicStreams#streamType(long) stream type} of the given + * streamId. + * The listener should return {@code true} if it wishes to acquire + * the stream. + * + * @param streamConsumer the listener + */ + public abstract void addRemoteStreamListener(Predicate streamConsumer); + + /** + * Removes a listener previously added with {@link #addRemoteStreamListener(Predicate)} + * @return {@code true} if the listener was found and removed, {@code false} otherwise + */ + public abstract boolean removeRemoteStreamListener(Predicate streamConsumer); + + /** + * {@return a stream of all currently opened {@link QuicStream} in the connection} + * + * @apiNote + * All current quic streams are included, whether local or remote, and whether they + * have been acquired or not. + * + * @see #addRemoteStreamListener(Predicate) + */ + public abstract Stream quicStreams(); + + /** + * {@return true if this connection is open} + */ + public abstract boolean isOpen(); + + /** + * {@return a long identifier that can be used to uniquely + * identify a quic connection in the context of the + * {@link QuicInstance} that created it} + */ + public long uniqueId() { return 0; } + + /** + * {@return a debug tag to be used with {@linkplain + * jdk.internal.net.http.common.Logger lower level logging}} + * This typically includes both the connection {@link #uniqueId()} + * and the {@link QuicInstance#instanceId()}. + */ + public abstract String dbgTag(); + + /** + * {@return a debug tag} + * Typically used with {@linkplain jdk.internal.net.http.common.Log + * higher level logging} + */ + public abstract String logTag(); + + /** + * {@return the {@link TerminationCause} if the connection has + * closed or is being closed, otherwise returns null} + */ + public abstract TerminationCause terminationCause(); + + public abstract QuicTLSEngine getTLSEngine(); + + public abstract InetSocketAddress peerAddress(); + + public abstract SocketAddress localAddress(); + + /** + * {@return a {@code CompletableFuture} that gets completed when + * the peer has acknowledged, or replied to the first {@link + * QuicPacket.PacketType#INITIAL INITIAL} + * packet + */ + public abstract CompletableFuture handshakeReachedPeer(); + + /** + * Requests to send a PING frame to the peer. + * An implementation may decide to support sending of out-of-band ping + * frames (triggered by the application layer) only for a subset of the + * {@linkplain jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace + * packet number spaces}. It may complete with -1 if it doesn't want to request + * sending of a ping frame at the time {@code requestSendPing()} is called. + * @return A completable future that will be completed with the number of + * milliseconds it took to get a valid response. It may also complete + * exceptionally, or with {@code -1L} if the ping was not sent. + */ + public abstract CompletableFuture requestSendPing(); + + /** + * {@return this connection {@code QuicConnectionId} or null} + * @implSpec + * The default implementation of this method returns null + */ + public QuicConnectionId localConnectionId() { return null; } + + /** + * {@return the {@link ConnectionTerminator} for this connection} + */ + public abstract ConnectionTerminator connectionTerminator(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java new file mode 100644 index 00000000000..21c40a69588 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + +/** + * Models a Quic Connection id. + * QuicConnectionId instance are typically created by a Quic client or server. + */ +// Connection IDs are used as keys in an ID to connection map. +// They implement Comparable to mitigate the penalty of hash collisions. +public abstract class QuicConnectionId implements Comparable { + + /** + * The maximum length, in bytes, of a connection id. + * This is supposed to be version-specific, but for now, we + * are going to treat that as a universal constant. + */ + public static final int MAX_CONNECTION_ID_LENGTH = 20; + protected final int hashCode; + protected final ByteBuffer buf; + + protected QuicConnectionId(ByteBuffer buf) { + this.buf = buf.asReadOnlyBuffer(); + hashCode = this.buf.hashCode(); + } + + /** + * Returns the length of this connection id, in bytes; + * @return the length of this connection id + */ + public int length() { + return buf.remaining(); + } + + /** + * Returns this connection id bytes as a read-only buffer. + * @return A new read only buffer containing this connection id bytes. + */ + public ByteBuffer asReadOnlyBuffer() { + return buf.asReadOnlyBuffer(); + } + + /** + * Returns this connection id bytes as a byte array. + * @return A new byte array containing this connection id bytes. + */ + public byte[] getBytes() { + var length = length(); + byte[] bytes = new byte[length]; + buf.get(buf.position(), bytes, 0, length); + return bytes; + } + + /** + * Compare this connection id bytes with the bytes in the + * given byte buffer. + *

    The given byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

    This method does not advance the buffer position. + * + * @implSpec This is equivalent to:

    {@code
    +     *  this.asReadOnlyBuffer().comparesTo(idbytes)
    +     *  }
    + * + * @param idbytes A byte buffer containing the id bytes of another + * connection id. + * @return {@code -1}, {@code 0}, or {@code 1} if this connection's id + * bytes are less, equal, or greater than the provided bytes. + */ + public int compareBytes(ByteBuffer idbytes) { + return buf.compareTo(idbytes); + } + + /** + * Tells whether the given byte buffer matches this connection id. + * The given byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

    This method does not advance the buffer position. + * + * @implSpec + * This is equivalent to:

    {@code
    +     *  this.asReadOnlyBuffer().mismatch(idbytes) == -1
    +     *  }
    + * + * @param idbytes A buffer that delimits a connection id. + * @return true if the bytes in the given buffer match this + * connection id bytes. + */ + public boolean matches(ByteBuffer idbytes) { + return buf.equals(idbytes); + } + + @Override + public int compareTo(QuicConnectionId o) { + return buf.compareTo(o.buf); + } + + + @Override + public final boolean equals(Object o) { + if (o instanceof QuicConnectionId that) { + return buf.equals(that.buf); + } + return false; + } + + @Override + public final int hashCode() { + return hashCode; + } + + /** + * {@return an hexadecimal string representing this connection id bytes, + * as returned by {@code HexFormat.of().formatHex(getBytes())}} + */ + public String toHexString() { + return HexFormat.of().formatHex(getBytes()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java new file mode 100644 index 00000000000..04cb2e6c263 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + +/** + * A class to generate connection ids bytes. + * This algorithm is specific to our implementation - it's not defined + * in any RFC (connection id bytes are free form). + * For the purpose of validation we encode the length of + * the connection id into the connection id bytes. + * For the purpose of uniqueness we encode a unique id. + * The rest of the connection id are random bytes. + */ +public class QuicConnectionIdFactory { + private static final Random RANDOM = new SecureRandom(); + private static final String CLIENT_DESC = "QuicClientConnectionId"; + private static final String SERVER_DESC = "QuicServerConnectionId"; + + private static final int MIN_CONNECTION_ID_LENGTH = 9; + + private final AtomicLong tokens = new AtomicLong(); + private volatile boolean wrapped; + private final byte[] scrambler; + private final Key statelessTokenKey; + private final String simpleDesc; + private final int connectionIdLength = RANDOM.nextInt(MIN_CONNECTION_ID_LENGTH, MAX_CONNECTION_ID_LENGTH+1); + + public static QuicConnectionIdFactory getClient() { + return new QuicConnectionIdFactory(CLIENT_DESC); + } + + public static QuicConnectionIdFactory getServer() { + return new QuicConnectionIdFactory(SERVER_DESC); + } + + private QuicConnectionIdFactory(String simpleDesc) { + this.simpleDesc = simpleDesc; + byte[] temp = new byte[MAX_CONNECTION_ID_LENGTH]; + RANDOM.nextBytes(temp); + scrambler = temp; + try { + KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256"); + statelessTokenKey = kg.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("HmacSHA256 key generator not available", e); + } + } + + /** + * The connection ID length used by this Quic instance. + * This is the source connection id length for outgoing packets, + * and the destination connection id length for incoming packets. + * @return the connection ID length used by this instance + */ + public int connectionIdLength() { + return connectionIdLength; + } + + /** + * Creates a new connection ID for a connection. + * @return a new connection ID + */ + public QuicConnectionId newConnectionId() { + long token = newToken(); + return new QuicLocalConnectionId(token, simpleDesc, + newConnectionId(connectionIdLength, token)); + } + + /** + * Quick validation to see if the buffer can contain a connection + * id generated by this instance. The byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

    This method does not advance the buffer position, and + * returns a connection id that wraps the given buffer. + * The returned connection id is only safe to use as long as + * the buffer is not modified. + *

    It is usually only used temporarily as a lookup key + * to locate an existing {@code QuicConnection}. + * + * @param buffer A buffer that delimits a connection id. + * @return a new QuicConnectionId if the buffer can contain + * a connection id generated by this instance, {@code null} + * otherwise. + */ + public QuicConnectionId unsafeConnectionIdFor(ByteBuffer buffer) { + int expectedLength = connectionIdLength; + + int remaining = buffer.remaining(); + if (remaining < MIN_CONNECTION_ID_LENGTH) return null; + if (remaining != expectedLength) return null; + + byte first = buffer.get(0); + int len = extractConnectionIdLength(first); + if (len < MIN_CONNECTION_ID_LENGTH) return null; + if (len > MAX_CONNECTION_ID_LENGTH) return null; + if (len != expectedLength) return null; + + long token = peekConnectionIdToken(buffer); + if (!isValidToken(token)) return null; + var cid = new QuicLocalConnectionId(buffer, token, simpleDesc); + assert cid.length() == expectedLength; + return cid; + } + + /** + * Returns a stateless reset token for the given connection ID + * @param connectionId connection ID + * @return stateless reset token for the given connection ID + * @throws IllegalArgumentException if the connection ID was not generated by this factory + */ + public byte[] statelessTokenFor(QuicConnectionId connectionId) { + if (!(connectionId instanceof QuicLocalConnectionId)) { + throw new IllegalArgumentException("Not a locally-generated connection ID"); + } + Mac mac; + try { + mac = Mac.getInstance("HmacSHA256"); + mac.init(statelessTokenKey); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("HmacSHA256 is not available", e); + } + byte[] result = mac.doFinal(connectionId.getBytes()); + return Arrays.copyOf(result, 16); + } + + // visible for testing + public long newToken() { + var token = tokens.incrementAndGet(); + if (token < 0) { + token = -token - 1; + wrapped = true; + } + return token; + } + + // visible for testing + public byte[] newConnectionId(int length, long token) { + length = Math.clamp(length, MIN_CONNECTION_ID_LENGTH, MAX_CONNECTION_ID_LENGTH); + assert length <= MAX_CONNECTION_ID_LENGTH; + assert length >= MIN_CONNECTION_ID_LENGTH; + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + + if (token < 0) token = -token - 1; + assert token >= 0; + int len = variableLengthLength(token); + assert len < 8; + + bytes[0] = (byte) ((length << 3) & 0xF8); + bytes[0] = (byte) (bytes[0] | len); + assert (bytes[0] & 0x07) == len; + assert ((bytes[0] & 0xFF) >> 3) == length : + "%s != %s".formatted(bytes[0] & 0xFF, length); + int shift = 8 * len; + for (int i = 0; i <= len; i++) { + assert shift <= 56; + bytes[i + 1] = (byte) ((token >> shift) & 0xFF); + shift -= 8; + } + for (int i = 0; i < length; i++) { + bytes[i] = (byte) ((bytes[i] & 0xFF) ^ (scrambler[i] & 0xFF)); + } + + assert length == getConnectionIdLength(bytes); + assert token == getConnectionIdToken(bytes); + return bytes; + } + + // visible for testing + public int getConnectionIdLength(byte[] bytes) { + assert bytes.length >= MIN_CONNECTION_ID_LENGTH; + var length = extractConnectionIdLength(bytes[0]); + assert length <= MAX_CONNECTION_ID_LENGTH; + return length; + } + + // visible for testing + public long getConnectionIdToken(byte[] bytes) { + assert bytes.length >= MIN_CONNECTION_ID_LENGTH; + int len = extractTokenLength(bytes[0]); + long token = 0; + int shift = len * 8; + for (int i = 0; i <= len; i++) { + assert shift >= 0; + assert shift <= 56; + int j = i + 1; + long l = ((bytes[j] & 0xFF) ^ (scrambler[j] & 0xFF)) & 0xFF; + l = l << shift; + token += l; + shift -= 8; + } + assert token >= 0; + return token; + } + + private long peekConnectionIdToken(ByteBuffer bytes) { + assert bytes.remaining() >= MIN_CONNECTION_ID_LENGTH; + int len = extractTokenLength(bytes.get(0)); + long token = 0; + int shift = len * 8; + for (int i = 0; i <= len; i++) { + assert shift >= 0; + assert shift <= 56; + int j = i + 1; + long l = ((bytes.get(j) & 0xFF) ^ (scrambler[j] & 0xFF)) & 0xFF; + l = l << shift; + token += l; + shift -= 8; + } + return token; + } + + private boolean isValidToken(long token) { + if (token < 0) return false; + long prevToken = tokens.get(); + boolean wrapped = prevToken < 0 || this.wrapped; + // if `tokens` has wrapped, we can say nothing... + // otherwise, we can say it should not be coded on more bytes than + // the previous token that was distributed + if (!wrapped) { + return token <= prevToken; + } + return true; + } + + private int extractConnectionIdLength(byte b) { + var bits = ((b & 0xFF) ^ (scrambler[0] & 0xFF)) & 0xFF; + bits = bits >> 3; + return bits; + } + + private int extractTokenLength(byte b) { + var bits = ((b & 0xFF) ^ (scrambler[0] & 0xFF)) & 0xFF; + return bits & 0x07; + } + + private static int variableLengthLength(long token) { + assert token >= 0; + int len = 0; + int shift = 0; + for (int i = 1; i < 8; i++) { + shift += 8; + if ((token >> shift) == 0) break; + len++; + } + assert len < 8; + return len; + } + + /** + * Checks if {@code connId} looks like a connection ID we could possibly generate. + * If it does, returns a stateless reset datagram. + * @param connId the destination connection id that was received on the packet + * @param length maximum length of the stateless reset packet + * @return stateless reset datagram payload, or null + */ + public ByteBuffer statelessReset(ByteBuffer connId, int length) { + // 43 bytes max: + // first byte bits 01xx xxxx + // followed by random bytes + // terminated by 16 bytes reset token + length = Math.min(length, 43); + if (length < 21) { // minimum QUIC short datagram length + return null; + } + + var cid = (QuicLocalConnectionId)unsafeConnectionIdFor(connId); + if (cid != null) { + var localToken = statelessTokenFor(cid); + assert localToken != null; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.put((byte)(0x40 + RANDOM.nextInt(0x40))); + byte[] random = new byte[length - 17]; + RANDOM.nextBytes(random); + buf.put(random); + buf.put(localToken); + assert !buf.hasRemaining() : buf.remaining(); + buf.flip(); + return buf; + } + return null; + } + + // A connection id generated by this instance. + private static final class QuicLocalConnectionId extends QuicConnectionId { + private final long token; + private final String simpleDesc; + + // Connection Ids created with this constructor are safer + // to use in maps as the buffer wraps a safe byte array in + // this constructor. + private QuicLocalConnectionId(long token, String simpleDesc, byte[] bytes) { + super(ByteBuffer.wrap(bytes)); + this.token = token; + this.simpleDesc = simpleDesc; + } + + // Connection Ids created with this constructor are only + // safe to use as long as the caller abstain from mutating + // the provided byte buffer. + // Typically, they will be transiently used to look up some + // connection in a map indexed by a connection id. + private QuicLocalConnectionId(ByteBuffer buffer, long token, String simpleDesc) { + super(buffer); + assert token >= 0; + this.token = token; + this.simpleDesc = simpleDesc; + } + + @Override + public String toString() { + return "%s(length=%s, token=%s, hash=%s)" + .formatted(simpleDesc, length(), token, hashCode); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java new file mode 100644 index 00000000000..f05519d339b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -0,0 +1,4353 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.net.ConnectException; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NetworkChannel; +import java.nio.channels.UnresolvedAddressException; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.OrderedFlow.CryptoDataFlow; +import jdk.internal.net.http.quic.QuicEndpoint.QuicDatagram; +import jdk.internal.net.http.quic.QuicTransportParameters.VersionInformation; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.DataBlockedFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.MaxDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.NewTokenFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PathChallengeFrame; +import jdk.internal.net.http.quic.frames.PathResponseFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.http.quic.packets.HandshakePacket; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.http.quic.packets.OneRttPacket; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder.OutgoingQuicPacket; +import jdk.internal.net.http.quic.packets.RetryPacket; +import jdk.internal.net.http.quic.packets.VersionNegotiationPacket; +import jdk.internal.net.http.quic.streams.CryptoWriterQueue; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicBidiStreamImpl; +import jdk.internal.net.http.quic.streams.QuicConnectionStreams; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStream.StreamState; +import jdk.internal.net.http.quic.streams.QuicStreams; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.HandshakeState; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.quic.QuicVersion; + +import static jdk.internal.net.http.quic.QuicClient.INITIAL_SERVER_CONNECTION_ID_LENGTH; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.active_connection_id_limit; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_data; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_local; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_source_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_idle_timeout; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_udp_payload_size; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.version_information; +import static jdk.internal.net.http.quic.TerminationCause.forException; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; +import static jdk.internal.net.http.quic.QuicRttEstimator.MAX_PTO_BACKOFF_TIMEOUT; +import static jdk.internal.net.http.quic.QuicRttEstimator.MIN_PTO_BACKOFF_TIMEOUT; +import static jdk.internal.net.http.quic.frames.QuicFrame.MAX_VL_INTEGER; +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static jdk.internal.net.http.quic.streams.QuicStreams.isUnidirectional; +import static jdk.internal.net.http.quic.streams.QuicStreams.streamType; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * This class implements a QUIC connection. + * A QUIC connection is established between a client and a server over a + * QuicEndpoint endpoint. + * A QUIC connection can then multiplex multiple QUIC streams to the same server. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +public class QuicConnectionImpl extends QuicConnection implements QuicPacketReceiver { + + private static final int MAX_IPV6_MTU = 65527; + private static final int MAX_IPV4_MTU = 65507; + + // Quic assumes a minimum packet size of 1200 + // See https://www.rfc-editor.org/rfc/rfc9000#name-datagram-size + public static final int SMALLEST_MAXIMUM_DATAGRAM_SIZE = + QuicClient.SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + public static final int DEFAULT_MAX_INITIAL_TIMEOUT = Math.clamp( + Utils.getIntegerProperty("jdk.httpclient.quic.maxInitialTimeout", 30), + 1, Integer.MAX_VALUE); + public static final long DEFAULT_INITIAL_MAX_DATA = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxInitialData", 15 << 20), + 0, 1L << 60); + public static final long DEFAULT_INITIAL_STREAM_MAX_DATA = Math.clamp( + Utils.getIntegerProperty("jdk.httpclient.quic.maxStreamInitialData", 6 << 20), + 0, 1L << 60); + public static final int DEFAULT_MAX_BIDI_STREAMS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxBidiStreams", 100); + public static final int DEFAULT_MAX_UNI_STREAMS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxUniStreams", 100); + public static final boolean USE_DIRECT_BUFFER_POOL = Utils.getBooleanProperty( + "jdk.internal.httpclient.quic.poolDirectByteBuffers", !QuicEndpoint.DGRAM_SEND_ASYNC); + + public static final int RESET_TOKEN_LENGTH = 16; // RFC states 16 bytes for stateless token + public static final long MAX_STREAMS_VALUE_LIMIT = 1L << 60; // cannot exceed 2^60 as per RFC + + // VarHandle provide the same atomic compareAndSet functionality + // than AtomicXXXXX classes, but without the additional cost in + // footprint. + private static final VarHandle VERSION_NEGOTIATED; + private static final VarHandle STATE; + private static final VarHandle MAX_SND_DATA; + private static final VarHandle MAX_RCV_DATA; + public static final int DEFAULT_DATAGRAM_SIZE; + private static final int MAX_INCOMING_CRYPTO_CAPACITY = 64 << 10; + + static { + try { + Lookup lookup = MethodHandles.lookup(); + VERSION_NEGOTIATED = lookup + .findVarHandle(QuicConnectionImpl.class, "versionNegotiated", boolean.class); + STATE = lookup.findVarHandle(QuicConnectionImpl.class, "state", int.class); + MAX_SND_DATA = lookup.findVarHandle(OneRttFlowControlledSendingQueue.class, "maxData", long.class); + MAX_RCV_DATA = lookup.findVarHandle(OneRttFlowControlledReceivingQueue.class, "maxData", long.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + int size = Utils.getIntegerProperty("jdk.httpclient.quic.defaultMTU", + SMALLEST_MAXIMUM_DATAGRAM_SIZE); + // don't allow the value to be below 1200 and above 65527, to conform with RFC-9000, + // section 18.2: + // The default for this parameter is the maximum permitted UDP payload of 65527. + // Values below 1200 are invalid. + if (size < SMALLEST_MAXIMUM_DATAGRAM_SIZE || size > MAX_IPV6_MTU) { + // fallback to SMALLEST_MAXIMUM_DATAGRAM_SIZE + size = SMALLEST_MAXIMUM_DATAGRAM_SIZE; + } + DEFAULT_DATAGRAM_SIZE = size; + } + + protected final Logger debug = Utils.getDebugLogger(this::dbgTag); + + final QuicRttEstimator rttEstimator = new QuicRttEstimator(); + final QuicCongestionController congestionController; + /** + * The state of the quic connection. + * The handshake is confirmed when HANDSHAKE_DONE has been received, + * or when the first 1-RTT packet has been successfully decrypted. + * See RFC 9001 section 4.1.2 + * https://www.rfc-editor.org/rfc/rfc9001#name-handshake-confirmed + */ + private final StateHandle stateHandle = new StateHandle(); + private final AtomicBoolean startHandshakeCalled = new AtomicBoolean(); + private final InetSocketAddress peerAddress; + private final QuicInstance quicInstance; + private final String dbgTag; + private final QuicTLSEngine quicTLSEngine; + private final CodingContext codingContext; + private final PacketSpaces packetSpaces; + private final OneRttFlowControlledSendingQueue oneRttSndQueue = + new OneRttFlowControlledSendingQueue(); + private final OneRttFlowControlledReceivingQueue oneRttRcvQueue = + new OneRttFlowControlledReceivingQueue(this::logTag); + protected final QuicConnectionStreams streams; + protected final Queue outgoing1RTTFrames = new ConcurrentLinkedQueue<>(); + // for one-rtt crypto data (session tickets) + private final CryptoDataFlow peerCryptoFlow = new CryptoDataFlow(); + private final CryptoWriterQueue localCryptoFlow = new CryptoWriterQueue(); + private final HandshakeFlow handshakeFlow = new HandshakeFlow(); + final ConnectionTerminatorImpl terminator; + protected final IdleTimeoutManager idleTimeoutManager; + protected final QuicTransportParameters transportParams; + // the initial (local) connection ID + private final QuicConnectionId connectionId; + private final PeerConnIdManager peerConnIdManager; + private final LocalConnIdManager localConnIdManager; + private volatile QuicConnectionId incomingInitialPacketSourceId; + protected final QuicEndpoint endpoint; + private volatile QuicTransportParameters localTransportParameters; + private volatile QuicTransportParameters peerTransportParameters; + private volatile byte[] initialToken; + // the number of (active) connection ids the peer is willing to accept for a given connection + private volatile long peerActiveConnIdsLimit = 2; // default is 2 as per RFC + + private volatile int state; + // the quic version currently in use + private volatile QuicVersion quicVersion; + // the quic version from the first packet + private final QuicVersion originalVersion; + private volatile QuicPacketDecoder decoder; + private volatile QuicPacketEncoder encoder; + // (client-only) if true, we no longer accept VERSIONS packets + private volatile boolean versionCompatible; + // if true, we no longer accept version changes + private volatile boolean versionNegotiated; + // true if we changed version in response to VERSIONS packet + private volatile boolean processedVersionsPacket; + // start off with 1200 or whatever is configured through + // jdk.net.httpclient.quic.defaultPDU system property + private int maxPeerAdvertisedPayloadSize = DEFAULT_DATAGRAM_SIZE; + // max MTU size on the connection: either MAX_IPV4_MTU or MAX_IPV6_MTU, + // depending on whether the peer address is IPv6 or IPv4 + private final int maxConnectionMTU; + // we start with a pathMTU that is 1200 or whatever is configured through + // jdk.net.httpclient.quic.defaultPDU system property + private int pathMTU = DEFAULT_DATAGRAM_SIZE; + private final SequentialScheduler handshakeScheduler = + SequentialScheduler.lockingScheduler(this::continueHandshake0); + private final ReentrantLock handshakeLock = new ReentrantLock(); + private final String cachedToString; + private final String logTag; + private final long labelId; + // incoming PATH_CHALLENGE frames waiting for PATH_RESPONSE + private final Queue pathChallengeFrameQueue = new ConcurrentLinkedQueue<>(); + + private volatile MaxInitialTimer maxInitialTimer; + + static String dbgTag(QuicInstance quicInstance, String logTag) { + return String.format("QuicConnection(%s, %s)", + quicInstance.instanceId(), logTag); + } + + protected QuicConnectionImpl(final QuicVersion firstFlightVersion, + final QuicInstance quicInstance, + final InetSocketAddress peerAddress, + final String peerName, + final int peerPort, + final SSLParameters sslParameters, + final String logTagFormat, + final long labelId) { + this.labelId = labelId; + this.quicInstance = Objects.requireNonNull(quicInstance, "quicInstance"); + try { + this.endpoint = quicInstance.getEndpoint(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + this.peerAddress = peerAddress; + this.maxConnectionMTU = peerAddress.getAddress() instanceof Inet6Address + ? MAX_IPV6_MTU + : MAX_IPV4_MTU; + this.pathMTU = Math.clamp(DEFAULT_DATAGRAM_SIZE, SMALLEST_MAXIMUM_DATAGRAM_SIZE, maxConnectionMTU); + this.cachedToString = String.format(logTagFormat.formatted("quic:%s:%s:%s"), labelId, + Arrays.toString(sslParameters.getApplicationProtocols()), peerAddress); + this.connectionId = this.endpoint.idFactory().newConnectionId(); + this.logTag = logTagFormat.formatted(labelId); + this.dbgTag = dbgTag(quicInstance, logTag); + this.congestionController = new QuicRenoCongestionController(dbgTag); + this.originalVersion = this.quicVersion = firstFlightVersion == null + ? QuicVersion.firstFlightVersion(quicInstance.getAvailableVersions()) + : firstFlightVersion; + final boolean isClientConn = isClientConnection(); + this.peerConnIdManager = new PeerConnIdManager(this, dbgTag); + this.localConnIdManager = new LocalConnIdManager(this, dbgTag, connectionId); + this.decoder = QuicPacketDecoder.of(this.quicVersion); + this.encoder = QuicPacketEncoder.of(this.quicVersion); + this.codingContext = new QuicCodingContext(); + final QuicTLSEngine engine = this.quicInstance.getQuicTLSContext() + .createEngine(peerName, peerPort); + engine.setUseClientMode(isClientConn); + engine.setSSLParameters(sslParameters); + this.quicTLSEngine = engine; + quicTLSEngine.setRemoteQuicTransportParametersConsumer(this::consumeQuicParameters); + packetSpaces = PacketSpaces.forConnection(this); + quicTLSEngine.setOneRttContext(packetSpaces.getOneRttContext()); + streams = new QuicConnectionStreams(this, debug); + if (quicInstance instanceof QuicClient quicClient) { + // use the (INITIAL) token that a server might have sent to this client (through + // NEW_TOKEN frame) on a previous connection against that server + this.initialToken = quicClient.initialTokenFor(this.peerAddress); + } + terminator = new ConnectionTerminatorImpl(this); + idleTimeoutManager = new IdleTimeoutManager(this); + transportParams = quicInstance.getTransportParameters() == null + ? new QuicTransportParameters() + : quicInstance.getTransportParameters(); + if (debug.on()) debug.log("Quic Connection Created"); + } + + @Override + public final long uniqueId() { + return labelId; + } + + /** + * An abstraction to represent the connection state as a bit mask. + * This is not an enum as some stages can overlap. + */ + public abstract static class QuicConnectionState { + public static final int + NEW = 0, // the connection is new + HISENT = 1, // first initial hello packet sent + HSCOMPLETE = 16, // handshake completed + CLOSING = 128, // connection has entered "Closing" state as defined in RFC-9000 + DRAINING = 256, // connection has entered "Draining" state as defined in RFC-9000 + CLOSED = 512; // CONNECTION_CLOSE ACK sent or received + public abstract int state(); + public boolean helloSent() {return isMarked(HISENT);} + public boolean handshakeComplete() { return isMarked(HSCOMPLETE);} + public boolean closing() { return isMarked(CLOSING);} + public boolean draining() { return isMarked(DRAINING);} + public boolean opened() { return (state() & (CLOSED | DRAINING | CLOSING)) == 0; } + public boolean isMarked(int mask) { return isMarked(state(), mask); } + public String toString() { return toString(state()); } + public static boolean isMarked(int state, int mask) { + return mask == 0 ? state == 0 : (state & mask) == mask; + } + public static String toString(int state) { + if (state == NEW) return "new"; + if (isMarked(state, CLOSED)) return "closed"; + if (isMarked(state, DRAINING)) return "draining"; + if (isMarked(state, CLOSING)) return "closing"; + if (isMarked(state, HSCOMPLETE)) return "handshakeComplete"; + if (isMarked(state, HISENT)) return "helloSent"; + return "Unknown(" + state + ")"; + } + } + + /** + * A {link QuicTimedEvent} used to interrupt the handshake + * if no response to the first initial packet is received within + * a reasonable delay (default is ~ 30s). + * This avoids waiting more than 30s for ConnectionException + * to be raised if no server is available at the peer address. + * This class is only used on the client side. + */ + final class MaxInitialTimer implements QuicTimedEvent { + private final Deadline maxInitialDeadline; + private final QuicTimerQueue timerQueue; + private final long eventId; + private volatile Deadline deadline; + private volatile boolean initialPacketReceived; + private volatile boolean connectionClosed; + + // optimization: if done is true it avoids volatile read + // of initialPacketReceived and/or connectionClosed + // from initialPacketReceived() + private boolean done; + private MaxInitialTimer(QuicTimerQueue timerQueue, Deadline maxDeadline) { + this.eventId = QuicTimerQueue.newEventId(); + this.timerQueue = timerQueue; + maxInitialDeadline = deadline = maxDeadline; + assert isClientConnection() : "MaxInitialTimer should only be used on QuicClients"; + } + + /** + * Called when an initial packet is received from the + * peer. At this point the MaxInitialTimer is disarmed, + * and further calls to this method are no-op. + */ + void initialPacketReceived() { + if (done) return; // races are OK - avoids volatile read + boolean firsPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + if (done = (firsPacketReceived || closed)) return; + initialPacketReceived = true; + if (debug.on()) { + debug.log("Quic initial timer disarmed after %s seconds", + DEFAULT_MAX_INITIAL_TIMEOUT - + Deadline.between(now(), maxInitialDeadline).toSeconds()); + } + if (!closed) { + // rescheduling with Deadline.MAX will take the + // MaxInitialTimer out of the timer queue. + timerQueue.reschedule(this, Deadline.MAX); + } + } + + @Override + public Deadline deadline() { + return deadline; + } + + /** + * This method is called if the timer expires. + * If no initial packet has been received ( + * {@link #initialPacketReceived()} was never called), + * the connection's handshakeCF is completed with a + * {@link ConnectException}. + * Calling this method a second time is a no-op. + * @return {@link Deadline#MAX}, always. + */ + @Override + public Deadline handle() { + if (done) return Deadline.MAX; + boolean firsPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + if (!firsPacketReceived && !closed) { + assert !now().isBefore(maxInitialDeadline); + var connectException = new ConnectException("No response from peer for %s seconds" + .formatted(DEFAULT_MAX_INITIAL_TIMEOUT)); + if (QuicConnectionImpl.this.handshakeFlow.handshakeCF() + .completeExceptionally(connectException)) { + // abandon the connection, but sends ConnectionCloseFrame + TerminationCause cause = TerminationCause.forException( + new QuicTransportException(connectException.getMessage(), + KeySpace.INITIAL, 0, QuicTransportErrors.APPLICATION_ERROR)); + terminator.terminate(cause); + } + connectionClosed = done = closed = true; + } + assert firsPacketReceived || closed; + return Deadline.MAX; + } + + @Override + public long eventId() { + return eventId; + } + + @Override + public Deadline refreshDeadline() { + boolean firstPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + Deadline newDeadlne = deadline; + if (closed || firstPacketReceived) newDeadlne = deadline = Deadline.MAX; + return newDeadlne; + } + + private Deadline now() { + return QuicConnectionImpl.this.endpoint().timeSource().instant(); + } + } + + /** + * A state handle is a mutable implementation of {@link QuicConnectionState} + * that allows to view the volatile connection int variable {@code state} as + * a {@code QuicConnectionState}, and provides methods to mutate it in + * a thread safe atomic way. + */ + protected final class StateHandle extends QuicConnectionState { + public int state() { return state;} + + /** + * Updates the state to a new state value with the passed bit {@code mask} set. + * + * @param mask The state mask + * @return true if previously the state value didn't have the {@code mask} set and this + * method successfully updated the state value to set the {@code mask} + */ + final boolean mark(final int mask) { + int state, desired; + do { + state = desired = state(); + if ((state & mask) == mask) return false; // already set + desired = state | mask; + } while (!STATE.compareAndSet(QuicConnectionImpl.this, state, desired)); + return true; // compareAndSet switched the old state to the desired state + } + public boolean markHelloSent() { return mark(HISENT); } + public boolean markHandshakeComplete() { return mark(HSCOMPLETE); } + } + + /** + * Keeps track of: + * - handshakeCF the handshake completable future + * - localInitial the local initial crypto writer queue + * - peerInitial the peer initial crypto flow + * - localHandshake the local handshake crypto queue + * - peerHandshake the peer handshake crypto flow + */ + protected final class HandshakeFlow { + + private final CompletableFuture handshakeCF; + // a CompletableFuture which will get completed when the handshake initiated locally, + // has "reached" the peer i.e. when the peer acknowledges or replies to the first + // INITIAL packet sent by an endpoint + final CompletableFuture handshakeReachedPeerCF; + private final CryptoWriterQueue localInitial = new CryptoWriterQueue(); + private final CryptoDataFlow peerInitial = new CryptoDataFlow(); + private final CryptoWriterQueue localHandshake = new CryptoWriterQueue(); + private final CryptoDataFlow peerHandshake = new CryptoDataFlow(); + private final AtomicBoolean handshakeStarted = new AtomicBoolean(); + + private HandshakeFlow() { + this.handshakeCF = new MinimalFuture<>(); + this.handshakeReachedPeerCF = new MinimalFuture<>(); + // ensure that the handshakeReachedPeerCF gets completed exceptionally + // if an exception is raised before the first INITIAL packet is + // acked by the peer. + handshakeCF.whenComplete((r, t) -> { + if (Log.quicHandshake()) { + Log.logQuic("{0} handshake completed {1}", + logTag(), + t == null ? "successfully" : ("exceptionally: " + t)); + } + if (t != null) { + handshakeReachedPeerCF.completeExceptionally(t); + } + }); + } + + /** + * {@return the CompletableFuture representing a handshake} + */ + public CompletableFuture handshakeCF() { + return this.handshakeCF; + } + + public void failHandshakeCFs(final Throwable cause) { + assert cause != null : "missing cause when failing handshake CFs"; + SSLHandshakeException sslHandshakeException = null; + if (!handshakeCF.isDone()) { + sslHandshakeException = sslHandshakeException(cause); + handshakeCF.completeExceptionally(sslHandshakeException); + } + if (!handshakeReachedPeerCF.isDone()) { + if (sslHandshakeException == null) { + sslHandshakeException = sslHandshakeException(cause); + } + handshakeReachedPeerCF.completeExceptionally(sslHandshakeException); + } + } + + private SSLHandshakeException sslHandshakeException(final Throwable cause) { + if (cause instanceof SSLHandshakeException ssl) { + return ssl; + } + return new SSLHandshakeException("QUIC connection establishment failed", cause); + } + + /** + * Marks the start of a handshake. + * @throws IllegalStateException If handshake has already started + */ + private void markHandshakeStart() { + if (!handshakeStarted.compareAndSet(false, true)) { + throw new IllegalStateException("Handshake has already started on " + + QuicConnectionImpl.this); + } + } + } + + public record PacketSpaces(PacketSpace initial, PacketSpace handshake, PacketSpace app) { + public PacketSpace get(PacketNumberSpace pnspace) { + return switch (pnspace) { + case INITIAL -> initial(); + case HANDSHAKE -> handshake(); + case APPLICATION -> app(); + default -> throw new IllegalArgumentException(String.valueOf(pnspace)); + }; + } + + private QuicOneRttContext getOneRttContext() { + final var appPacketSpaceMgr = app(); + assert appPacketSpaceMgr instanceof QuicOneRttContext + : "unexpected 1-RTT packet space manager"; + return (QuicOneRttContext) appPacketSpaceMgr; + } + + public static PacketSpaces forConnection(final QuicConnectionImpl connection) { + final var initialPktSpaceMgr = new PacketSpaceManager(connection, PacketNumberSpace.INITIAL); + return new PacketSpaces(initialPktSpaceMgr, + new PacketSpaceManager.HandshakePacketSpaceManager(connection, initialPktSpaceMgr), + new PacketSpaceManager.OneRttPacketSpaceManager(connection)); + } + + public void close() { + initial.close(); + handshake.close(); + app.close(); + } + } + + private final ConcurrentLinkedQueue incoming = new ConcurrentLinkedQueue<>(); + private final SequentialScheduler incomingLoopScheduler = + SequentialScheduler.lockingScheduler(this::incoming); + + + /* + * delegate handling of the datagrams to the executor to free up + * the endpoint readLoop. Helps with processing ACKs in a more + * timely fashion, which avoids too many retransmission. + * The endpoint readLoop runs on a single thread, while this loop + * will have one thread per connection which helps with a better + * utilization of the system resources. + */ + private void scheduleForDecryption(IncomingDatagram datagram) { + // Processes an incoming encrypted packet that has just been + // read off the network. + var received = datagram.buffer.remaining(); + if (incomingLoopScheduler.isStopped()) { + if (debug.on()) { + debug.log("scheduleForDecryption closed: dropping datagram (%d bytes)", + received); + } + return; + } + if (debug.on()) { + debug.log("scheduleForDecryption: %d bytes", received); + } + endpoint.buffer(received); + incoming.add(datagram); + + incomingLoopScheduler.runOrSchedule(quicInstance().executor()); + } + + private void incoming() { + try { + IncomingDatagram datagram; + while ((datagram = incoming.poll()) != null) { + ByteBuffer buffer = datagram.buffer; + int remaining = buffer.remaining(); + try { + if (incomingLoopScheduler.isStopped()) { + // we still need to unbuffer, continue here will + // ensure we skip directly to the finally-block + // below. + continue; + } + + internalProcessIncoming(datagram.source(), + datagram.destConnId(), + datagram.headersType(), + datagram.buffer()); + } catch (Throwable t) { + if (Log.errors() || debug.on()) { + String msg = "Failed to process datagram: " + t; + Log.logError(logTag() + " " + msg); + debug.log(msg, t); + } + } finally { + endpoint.unbuffer(remaining); + } + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * Schedule an incoming quic packet for decryption. + * The ByteBuffer should contain a single packet, and its + * limit should be set at the end of the packet. + * + * @param buffer a byte buffer containing the incoming packet + */ + private void decrypt(ByteBuffer buffer) { + // Processes an incoming encrypted packet that has just been + // read off the network. + PacketType packetType = decoder.peekPacketType(buffer); + var received = buffer.remaining(); + var pos = buffer.position(); + if (debug.on()) { + debug.log("decrypt %s(pos=%d, remaining=%d)", + packetType, pos, received); + } + try { + assert packetType != PacketType.VERSIONS; + var packet = codingContext.parsePacket(buffer); + if (packet != null) { + processDecrypted(packet); + } else { + if (packetType == PacketType.HANDSHAKE) { + packetSpaces.initial.fastRetransmit(); + } + } + } catch (QuicTransportException qte) { + // close the connection on this fatal error + if (Log.errors() || debug.on()) { + final String msg = "closing connection due to error while decoding" + + " packet (type=" + packetType + "): " + qte; + Log.logError(logTag() + " " + msg); + debug.log(msg, qte); + } + terminator.terminate(TerminationCause.forException(qte)); + } catch (Throwable t) { + if (Log.errors() || debug.on()) { + String msg = "Failed to decode packet (type=" + packetType + "): " + t; + Log.logError(logTag() + " " + msg); + debug.log(msg, t); + } + } + } + + public void closeIncoming() { + incomingLoopScheduler.stop(); + IncomingDatagram icd; + // we still need to unbuffer all datagrams in the queue + while ((icd = incoming.poll()) != null ) { + endpoint.unbuffer(icd.buffer().remaining()); + } + } + + /** + * A protection record contains a packet to encrypt, and a datagram that may already + * contain encrypted packets. The firstPacketOffset indicates the position of the + * first encrypted packet in the datagram. The packetOffset indicates the position + * at which this packet will be - or has been - written in the datagram. + * Before the packet is encrypted and written to the datagram, the packetOffset + * should be the same as the datagram buffer position. + * After the packet has been written, the packetOffset should indicate + * at which position the packet has been written. The datagram position + * indicates where to write the next packet. + *

    + * Additionally, a {@code ProtectionRecord} may carry some flags indicating the + * intended usage of the datagram. The following flags are supported: + *

      + *
    • {@link #SINGLE_PACKET}: the default - it is not expected that the + * datagram will contain more packets
    • + *
    • {@link #COALESCED}: should be used if it is expected that the + * datagram will contain more than one packet
    • + *
    • {@link #LAST_PACKET}: should be used in conjunction with {@link #COALESCED} + * to indicate that the packet being protected is the last that will be + * added to the datagram
    • + *
    + * + * @apiNote + * Flag values can be combined, but some combinations + * may not make sense. A single packet can also be identified as any + * packet that doesn't have the {@code COALESCED} bit on. + * The flag is used to convey information that may be used to figure + * out whether to send the datagram right away, or whether to wait for + * more packet to be coalesced inside it. + * + * @param packet the packet to encrypt + * @param datagram the datagram in which the encrypted packet should be written + * @param firstPacketOffset the position of the first encrypted packet in the datagram + * @param packetOffset the offset at which the packet should be / has been written in the datagram + * @param flags a bit mask containing some details about the datagram being sent out + */ + public record ProtectionRecord(QuicPacket packet, ByteBuffer datagram, + int firstPacketOffset, int packetOffset, + long retransmittedPacketNumber, int flags) { + /** + * This is the default. + * This protection record is adding a single packet to be sent into + * the datagram and the datagram can be sent as soon as the packet + * has been encrypted. + */ + public static final int SINGLE_PACKET = 0; + /** + * This can be used when it is expected that more than one packet + * will be added to this datagram. We should wait until the last packet + * has been added before sending the datagram out. + */ + public static final int COALESCED = 1; + /** + * This protection record is adding the last packet to be sent into + * the datagram and the datagram can be sent as soon as the packet + * has been encrypted. + */ + public static final int LAST_PACKET = 2; + + // indicate that the packet is not retransmitted + private static final long NOT_RETRANSMITTED = -1L; + + ProtectionRecord withOffset(int packetOffset) { + if (this.packetOffset == packetOffset) { + return this; + } + return new ProtectionRecord(packet, datagram, firstPacketOffset, + packetOffset, retransmittedPacketNumber, flags); + } + + public ProtectionRecord encrypt(final CodingContext codingContext) + throws QuicKeyUnavailableException, QuicTransportException { + final PacketType packetType = packet.packetType(); + assert packetType != PacketType.VERSIONS; + // keep track of position before encryption + final int preEncryptPos = datagram.position(); + codingContext.writePacket(packet, datagram); + final ProtectionRecord encrypted = withOffset(preEncryptPos); + return encrypted; + } + + /** + * Records the intent of protecting a packet that will be sent as soon + * as it has been encrypted, without waiting for more packets to be + * coalesced into the datagram. + * + * @param packet the packet to protect + * @param allocator an allocator to allocate the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord single(QuicPacket packet, + Function allocator) { + ByteBuffer datagram = allocator.apply(packet); + int offset = datagram.position(); + return new ProtectionRecord(packet, datagram, + offset, offset, NOT_RETRANSMITTED, 0); + } + + /** + * Records the intent of protecting a packet that retransmits + * a previously transmitted packet. The packet will be sent as soon + * as it has been encrypted, without waiting for more packets to be + * coalesced into the datagram. + * + * @param packet the packet to protect + * @param retransmittedPacketNumber the packet number of the original + * packet that was considered lost + * @param allocator an allocator to allocate the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord retransmitting(QuicPacket packet, + long retransmittedPacketNumber, + Function allocator) { + ByteBuffer datagram = allocator.apply(packet); + int offset = datagram.position(); + return new ProtectionRecord(packet, datagram, offset, offset, + retransmittedPacketNumber, 0); + } + + /** + * Records the intent of protecting a packet that will be followed by + * more packets to be coalesced in the same datagram. The datagram + * should not be sent until the last packet has been coalesced. + * + * @param packet the packet to protect + * @param datagram the datagram in which packet will be coalesced + * @param firstPacketOffset the offset of the first packet in the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord more(QuicPacket packet, ByteBuffer datagram, int firstPacketOffset) { + return new ProtectionRecord(packet, datagram, firstPacketOffset, + datagram.position(), NOT_RETRANSMITTED, COALESCED); + } + + /** + * Records the intent of protecting the last packet that will be + * coalesced in the given datagram. The datagram can be sent as soon + * as the packet has been encrypted and coalesced into the given + * datagram. + * + * @param packet the packet to protect + * @param datagram the datagram in which packet will be coalesced + * @param firstPacketOffset the offset of the first packet in the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord last(QuicPacket packet, ByteBuffer datagram, int firstPacketOffset) { + return new ProtectionRecord(packet, datagram, firstPacketOffset, + datagram.position(), NOT_RETRANSMITTED, LAST_PACKET | COALESCED); + } + } + + final QuicPacket newQuicPacket(final KeySpace keySpace, final List frames) { + final PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.of(keySpace)); + return encoder.newOutgoingPacket(keySpace, packetSpace, + localConnectionId(), peerConnectionId(), initialToken(), + frames, + codingContext); + } + + /** + * Encrypt an outgoing quic packet. + * The ProtectionRecord indicates the position at which the encrypted packet + * should be written in the datagram, as well as the position of the + * first packet in the datagram. After encrypting the packet, this method calls + * {@link #pushEncryptedDatagram(ProtectionRecord)} + * + * @param protectionRecord a record containing a quic packet to encrypt, + * a destination byte buffer, and various offset information. + */ + final void pushDatagram(final ProtectionRecord protectionRecord) + throws QuicKeyUnavailableException, QuicTransportException { + final QuicPacket packet = protectionRecord.packet(); + if (debug.on()) { + debug.log("encrypting packet into datagram %s(pn:%s, %s)", packet.packetType(), + packet.packetNumber(), packet.frames()); + } + // Processes an outgoing unencrypted packet that needs to be + // encrypted before being packaged in a datagram. + final ProtectionRecord encrypted; + try { + encrypted = protectionRecord.encrypt(codingContext); + } catch (Throwable e) { + // release the datagram ByteBuffer on failure to encrypt + datagramDiscarded(new QuicDatagram(this, peerAddress, protectionRecord.datagram())); + if (Log.errors()) { + Log.logError("Failed to encrypt packet: " + e); + // certain failures like key not being available are OK + // in some situations. log the stacktrace only if this + // was an unexpected failure. + boolean skipStackTrace = false; + if (e instanceof QuicKeyUnavailableException) { + final PacketSpace packetSpace = packetSpace(protectionRecord.packet().numberSpace()); + skipStackTrace = packetSpace.isClosed(); + } + if (!skipStackTrace) { + Log.logError(e); + } + } + throw e; + } + // we currently don't support a ProtectionRecord with more than one QuicPacket + assert (encrypted.flags & ProtectionRecord.COALESCED) == 0 : "coalesced packets not supported"; + // encryption of the datagram is complete, now push the encrypted + // datagram through the endpoint + if (Log.quicPacketOutLoggable(packet)) { + Log.logQuicPacketOut(logTag(), packet); + } + pushEncryptedDatagram(encrypted); + } + + protected void completeHandshakeCF() { + // This can be called from the decrypt loop, and can trigger + // sending of 1-RTT application data from within the same + // thread: we use an executor here to avoid running the application + // sending loop from within the Quic decrypt loop. + completeHandshakeCF(quicInstance().executor()); + } + + protected final void completeHandshakeCF(Executor executor) { + final var handshakeCF = handshakeFlow.handshakeCF(); + if (handshakeCF.isDone()) { + return; + } + var handshakeState = quicTLSEngine.getHandshakeState(); + if (executor != null) { + handshakeCF.completeAsync(() -> handshakeState, executor); + } else { + handshakeCF.complete(handshakeState); + } + } + + /** + * A class used to check that 1-RTT received data doesn't exceed + * the MAX_DATA of the connection + */ + class OneRttFlowControlledReceivingQueue { + private static final long MIN_BUFFER_SIZE = 16L << 10; // 16k + private volatile long receivedData; + private volatile long maxData; + private volatile long processedData; + // Desired buffer size; used when updating maxStreamData + private final long desiredBufferSize = Math.clamp(DEFAULT_INITIAL_MAX_DATA, MIN_BUFFER_SIZE, MAX_VL_INTEGER); + private final Supplier logTag; + + OneRttFlowControlledReceivingQueue(Supplier logTag) { + this.logTag = Objects.requireNonNull(logTag); + } + + /** + * Called when new local parameters are available + * @param localParameters the new local paramaters + */ + void newLocalParameters(QuicTransportParameters localParameters) { + if (localParameters.isPresent(ParameterId.initial_max_data)) { + long maxData = this.maxData; + long newMaxData = localParameters.getIntParameter(ParameterId.initial_max_data); + while (maxData < newMaxData) { + if (MAX_RCV_DATA.compareAndSet(this, maxData, newMaxData)) break; + maxData = this.maxData; + } + } + } + + /** + * Checks whether the give frame would cause the connection max data + * to be exceeded. If no, increase the amount of data processed by + * this connection by the length of the frame. If yes, sends a + * ConnectionCloseFrame with FLOW_CONTROL_ERROR. + * + * @param diff number of bytes newly received + * @param frameType type of frame received + * @throws QuicTransportException if processing this frame would cause the connection + * max data to be exceeded + */ + void checkAndIncreaseReceivedData(long diff, long frameType) throws QuicTransportException { + assert diff > 0; + long max, processed; + boolean exceeded; + synchronized (this) { + max = maxData; + processed = receivedData; + if (max - processed < diff) { + exceeded = true; + } else { + try { + receivedData = processed = Math.addExact(processed, diff); + exceeded = false; + } catch (ArithmeticException x ) { + // should not happen - flow control should have + // caught that + receivedData = processed = Long.MAX_VALUE; + exceeded = true; + } + } + } + if (exceeded) { + String reason = "Connection max data exceeded: max data processed=%s, max connection data=%s" + .formatted(processed, max); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, frameType, QuicTransportErrors.FLOW_CONTROL_ERROR); + } + } + + public void increaseProcessedData(long diff) { + long processed, received, max; + synchronized (this) { + processed = processedData += diff; + received = receivedData; + max = maxData; + } + if (Log.quicProcessed()) { + Log.logQuic(logTag()+ " Processed: " + processed + + ", received: " + received + + ", max:" + max); + } + if (needSendMaxData()) { + runAppPacketSpaceTransmitter(); + } + } + + private long bumpMaxData() { + long newMaxData = processedData + desiredBufferSize; + long maxData = this.maxData; + if (newMaxData - maxData < (desiredBufferSize / 5)) { + return 0; + } + while (maxData < newMaxData) { + if (MAX_RCV_DATA.compareAndSet(this, maxData, newMaxData)) + return newMaxData; + maxData = this.maxData; + } + return 0; + } + + public boolean needSendMaxData() { + return maxData - processedData < desiredBufferSize/2; + } + + String logTag() { return logTag.get(); } + } + + /** + * An event loop triggered when stream data is available for sending. + * We use a sequential scheduler here to make sure we don't send + * more data than allowed by the connection's flow control. + * This guarantee that only one thread composes flow controlled + * OneRTT packets at a given time, which in turn guarantees that the + * credit computed at the beginning of the loop will still be + * available after the packet has been composed. + */ + class OneRttFlowControlledSendingQueue { + private volatile long dataProcessed; + private volatile long maxData; + + /** + * Called when a MAX_DATA frame is received. + * This method is a no-op if the given value is less than the + * current max stream data for the connection. + * + * @param maxData the maximum data offset that the peer is prepared + * to accept for the whole connection + * @param isInitial true when processing transport parameters, + * false when processing MaxDataFrame + * @return the actual max data after taking the given value into account + */ + public long setMaxData(long maxData, boolean isInitial) { + long max; + long processed; + boolean wasblocked, unblocked = false; + do { + synchronized (this) { + max = this.maxData; + processed = dataProcessed; + } + wasblocked = max <= processed; + if (max < maxData) { + if (MAX_SND_DATA.compareAndSet(this, max, maxData)) { + max = maxData; + unblocked = (wasblocked && max > processed); + } + } + } while (max < maxData); + if (unblocked && !isInitial) { + packetSpaces.app.runTransmitter(); + } + return max; + } + + /** + * {@return the remaining credit for this connection} + */ + public long credit() { + synchronized (this) { + return maxData - dataProcessed; + } + } + + // We can continue sending if we have credit and data is available to send + private boolean canSend() { + return credit() > 0 && streams.hasAvailableData() + || streams.hasControlFrames() + || hasQueuedFrames() + || oneRttRcvQueue.needSendMaxData(); + } + + // implementation of the sending loop. + private boolean send1RTTData() { + Throwable failure; + try { + return doSend1RTTData(); + } catch (Throwable t) { + failure = t; + } + if (failure instanceof QuicKeyUnavailableException qkue) { + if (!QuicConnectionImpl.this.stateHandle().opened()) { + // connection is already being closed and that explains the + // key unavailability (they might have been discarded). just log + // and return + if (debug.on()) { + debug.log("failed to send stream data, reason: " + qkue.getMessage()); + } + return false; + } + // connection is still open but a key unavailability exception was raised. + // close the connection and use an IOException instead of the internal + // QuicKeyUnavailableException as the cause for the connection close. + failure = new IOException(qkue.getMessage()); + } + if (debug.on()) { + debug.log("failed to send stream data", failure); + } + // close the connection to make sure it's not just ignored + terminator.terminate(TerminationCause.forException(failure)); + return false; + } + + private boolean doSend1RTTData() throws QuicKeyUnavailableException, QuicTransportException { + // Loop over all sending streams to see if data is available - include + // as much data as possible in the quic packet before sending it. + // The QuicConnectionStreams make sure that streams are polled in a fair + // manner (using round-robin?) + // This loop is called through a sequential scheduler to make + // sure we only have one thread emitting flow control data for + // this connection + final PacketSpace space = packetSpace(PacketNumberSpace.APPLICATION); + final int maxDatagramSize = getMaxDatagramSize(); + final QuicConnectionId peerConnectionId = peerConnectionId(); + final int dstIdLength = peerConnectionId().length(); + if (!canSend()) { + return false; + } + final long packetNumber = space.allocateNextPN(); + final long largestPeerAckedPN = space.getLargestPeerAckedPN(); + int remaining = QuicPacketEncoder.computeMaxOneRTTPayloadSize( + codingContext, packetNumber, dstIdLength, maxDatagramSize, largestPeerAckedPN); + if (remaining == 0) { + // not enough space to send available data + return false; + } + final List frames = new ArrayList<>(); + remaining -= addConnectionControlFrames(remaining, frames); + assert remaining >= 0 : remaining; + long produced = streams.produceFramesToSend(encoder, remaining, credit(), frames); + if (frames.isEmpty()) { + // produced cannot be > 0 unless there are some frames to send + assert produced == 0; + return false; + } + // non-atomic operation should be OK since sendStreamData0 is called + // only from the sending loop, and this is the only place where we + // mutate dataProcessed. + dataProcessed += produced; + final OneRttPacket packet = encoder.newOneRttPacket(peerConnectionId, + packetNumber, largestPeerAckedPN, frames, codingContext); + QuicConnectionImpl.this.send1RTTPacket(packet); + return true; + } + + /** + * Produces connection-level control frames for sending in the next one-rtt + * packet. The frames are added to the provided list. + * + * @param maxAllowedBytes maximum number of bytes the method is allowed to add + * @param frames list where the frames are added + * @return number of bytes added + */ + private int addConnectionControlFrames(final int maxAllowedBytes, + final List frames) { + assert maxAllowedBytes > 0 : "unexpected max allowed bytes: " + maxAllowedBytes; + int added = 0; + int remaining = maxAllowedBytes; + QuicFrame f; + while ((f = outgoing1RTTFrames.peek()) != null) { + final int frameSize = f.size(); + if (frameSize <= remaining) { + outgoing1RTTFrames.remove(); + frames.add(f); + added += frameSize; + remaining -= frameSize; + } else { + break; + } + } + PathChallengeFrame pcf; + while (remaining >= 9 && (pcf = pathChallengeFrameQueue.poll()) != null) { + f = new PathResponseFrame(pcf.data()); + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + + // NEW_CONNECTION_ID + while ((f = localConnIdManager.nextFrame(remaining)) != null) { + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + // RETIRE_CONNECTION_ID + while ((f = peerConnIdManager.nextFrame(remaining)) != null) { + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + + if (remaining == 0) { + return added; + } + final PacketSpace space = packetSpace(PacketNumberSpace.APPLICATION); + final AckFrame ack = space.getNextAckFrame(false, remaining); + if (ack != null) { + final int ackFrameSize = ack.size(); + assert ackFrameSize <= remaining; + if (debug.on()) { + debug.log("Adding AckFrame"); + } + frames.add(ack); + added += ackFrameSize; + remaining -= ackFrameSize; + } + final long credit = credit(); + if (credit < remaining && remaining > 10) { + if (debug.on()) { + debug.log("Adding DataBlockedFrame"); + } + DataBlockedFrame dbf = new DataBlockedFrame(maxData); + frames.add(dbf); + added += dbf.size(); + remaining -= dbf.size(); + } + // max data + if (remaining > 10) { + long maxData = oneRttRcvQueue.bumpMaxData(); + if (maxData != 0) { + if (debug.on()) { + debug.log("Adding MaxDataFrame (processed: %s)", + oneRttRcvQueue.processedData); + } + MaxDataFrame mdf = new MaxDataFrame(maxData); + frames.add(mdf); + added += mdf.size(); + remaining -= mdf.size(); + } + } + // session ticket + if (quicTLSEngine.getCurrentSendKeySpace() == KeySpace.ONE_RTT) { + try { + ByteBuffer payloadBuffer = quicTLSEngine.getHandshakeBytes(KeySpace.ONE_RTT); + if (payloadBuffer != null) { + localCryptoFlow.enqueue(payloadBuffer); + } + } catch (IOException e) { + throw new AssertionError("Should not happen!", e); + } + if (localCryptoFlow.remaining() > 0 && remaining > 3) { + CryptoFrame frame = localCryptoFlow.produceFrame(remaining); + if (frame != null) { + if (debug.on()) { + debug.log("Adding CryptoFrame"); + } + frames.add(frame); + added += frame.size(); + remaining -= frame.size(); + assert remaining >= 0; + } + } + } + return added; + } + } + + /** + * Invoked to send a ONERTT packet containing stream data or + * control frames. + * + * @apiNote + * This method can be overridden if some action needs to be + * performed after sending a packet containing certain type + * of frames. Typically, a server side connection may want + * to close the HANDSHAKE space only after sending the + * HANDSHAKE_DONE frame. + * + * @param packet The ONERTT packet to send. + */ + protected void send1RTTPacket(final OneRttPacket packet) + throws QuicKeyUnavailableException, QuicTransportException { + pushDatagram(ProtectionRecord.single(packet, + QuicConnectionImpl.this::allocateDatagramForEncryption)); + } + + /** + * Schedule a frame for sending in a 1-RTT packet. + *

    + * For use with frames that do not change with time + * (like MAX_* / *_BLOCKED / ACK), + * or with remaining datagram capacity (like STREAM or CRYPTO), + * and do not require certain path (PATH_CHALLENGE / RESPONSE). + *

    + * Use with frames like HANDSHAKE_DONE, NEW_TOKEN, + * NEW_CONNECTION_ID, RETIRE_CONNECTION_ID. + *

    + * Maximum accepted frame size is 1000 bytes to ensure that the frame + * will fit in a 1-RTT datagram in the foreseeable future. + * @param frame frame to send + * @throws IllegalArgumentException if frame is larger than 1000 bytes + */ + protected void enqueue1RTTFrame(final QuicFrame frame) { + if (frame.size() > 1000) { + throw new IllegalArgumentException("Frame too big"); + } + assert frame.isValidIn(PacketType.ONERTT) : "frame " + frame + " is not" + + " eligible in 1-RTT space"; + outgoing1RTTFrames.add(frame); + } + + /** + * {@return true if queued frames are available for sending} + */ + private boolean hasQueuedFrames() { + return !outgoing1RTTFrames.isEmpty(); + } + + protected QuicPacketEncoder encoder() { return encoder;} + protected QuicPacketDecoder decoder() { return decoder; } + public QuicEndpoint endpoint() { return endpoint; } + protected final StateHandle stateHandle() { return stateHandle; } + protected CodingContext codingContext() { + return codingContext; + } + + public long largestAckedPN(PacketNumberSpace packetSpace) { + var space = packetSpaces.get(packetSpace); + return space.getLargestPeerAckedPN(); + } + + public long largestProcessedPN(PacketNumberSpace packetSpace) { + var space = packetSpaces.get(packetSpace); + return space.getLargestProcessedPN(); + } + + public int connectionIdLength() { + return localConnectionId().length(); + } + + public QuicInstance quicInstance() { + return this.quicInstance; + } + + public QuicVersion quicVersion() { + return this.quicVersion; + } + + protected class QuicCodingContext implements CodingContext { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return QuicConnectionImpl.this.largestProcessedPN(packetSpace); + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return QuicConnectionImpl.this.largestAckedPN(packetSpace); + } + @Override public int connectionIdLength() { + return QuicConnectionImpl.this.connectionIdLength(); + } + @Override public int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException { + int start = buffer.position(); + encoder.encode(packet, buffer, this); + return buffer.position() - start; + } + @Override public QuicPacket parsePacket(ByteBuffer src) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + return decoder.decode(src, this); + } + @Override + public QuicConnectionId originalServerConnId() { + return QuicConnectionImpl.this.originalServerConnId(); + } + + @Override + public QuicTLSEngine getTLSEngine() { + return quicTLSEngine; + } + + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return QuicConnectionImpl.this.verifyToken(destinationID, token); + } + } + + protected boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + // server must send zero-length token + return token == null; + } + + protected PacketEmitter emitter() { + return new PacketEmitter() { + @Override + public QuicTimerQueue timer() { + return QuicConnectionImpl.this.endpoint().timer(); + } + + @Override + public void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException { + QuicConnectionImpl.this.retransmit(packetSpaceManager, packet, attempts); + } + + @Override + public long emitAckPacket(PacketSpace packetSpaceManager, + AckFrame frame, + boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + return QuicConnectionImpl.this.emitAckPacket(packetSpaceManager, frame, sendPing); + } + + @Override + public void acknowledged(QuicPacket packet) { + QuicConnectionImpl.this.packetAcknowledged(packet); + } + + @Override + public boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + return QuicConnectionImpl.this.sendData(packetNumberSpace); + } + + @Override + public Executor executor() { + return quicInstance().executor(); + } + + @Override + public void reschedule(QuicTimedEvent task) { + var endpoint = QuicConnectionImpl.this.endpoint(); + if (endpoint == null) return; + endpoint.timer().reschedule(task); + } + + @Override + public void reschedule(QuicTimedEvent task, Deadline deadline) { + var endpoint = QuicConnectionImpl.this.endpoint(); + if (endpoint == null) return; + endpoint.timer().reschedule(task, deadline); + } + + @Override + public void checkAbort(PacketNumberSpace packetNumberSpace) { + QuicConnectionImpl.this.checkAbort(packetNumberSpace); + } + + @Override + public void ptoBackoffIncreased(PacketSpaceManager space, long backoff) { + if (Log.quicRetransmit()) { + Log.logQuic("%s OUT: [%s] increase backoff to %s, duration %s ms: %s" + .formatted(QuicConnectionImpl.this.logTag(), + space.packetNumberSpace(), backoff, + space.getPtoDuration().toMillis(), + rttEstimator.state())); + } + } + + @Override + public String logTag() { + return QuicConnectionImpl.this.logTag(); + } + + @Override + public boolean isOpen() { + return QuicConnectionImpl.this.stateHandle.opened(); + } + }; + } + + private void checkAbort(PacketNumberSpace packetNumberSpace) { + // if pto backoff > 32 (i.e. PTO expired 5 times in a row), abort, + // unless we haven't reached MIN_PTO_BACKOFF_TIMEOUT + var backoff = rttEstimator.getPtoBackoff(); + if (backoff > QuicRttEstimator.MAX_PTO_BACKOFF) { + // If the maximum backoff is exceeded, we close the connection + // only if the associated backoff timeout exceeds the + // MIN_PTO_BACKOFF_TIMEOUT. Otherwise, we allow the backoff + // factor to grow again past the MAX_PTO_BACKOFF + if (rttEstimator.isMinBackoffTimeoutExceeded()) { + if (debug.on()) { + debug.log("%s Too many probe time outs: %s", packetNumberSpace, backoff); + debug.log(String.valueOf(rttEstimator.state())); + debug.log("State: %s", stateHandle().toString()); + } + if (Log.quicRetransmit() || Log.quicCC()) { + Log.logQuic("%s OUT: %s: Too many probe timeouts %s" + .formatted(logTag(), packetNumberSpace, + rttEstimator.state())); + StringBuilder sb = new StringBuilder(logTag()); + sb.append(" State: ").append(stateHandle().toString()); + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (packetSpaces.get(sp) instanceof PacketSpaceManager m) { + sb.append("\nPacketSpace: ").append(sp).append('\n'); + m.debugState(" ", sb); + } + } + Log.logQuic(sb.toString()); + } else if (debug.on()) { + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (packetSpaces.get(sp) instanceof PacketSpaceManager m) { + m.debugState(); + } + } + } + var pto = rttEstimator.getBasePtoDuration(); + var to = pto.multipliedBy(backoff); + if (to.compareTo(MAX_PTO_BACKOFF_TIMEOUT) > 0) to = MAX_PTO_BACKOFF_TIMEOUT; + String msg = "%s: Too many probe time outs (%s: backoff %s, duration %s, %s)" + .formatted(logTag(), packetNumberSpace, backoff, + to, rttEstimator.state()); + final TerminationCause terminationCause; + if (packetNumberSpace == PacketNumberSpace.HANDSHAKE) { + terminationCause = TerminationCause.forException(new SSLHandshakeException(msg)); + } else if (packetNumberSpace == PacketNumberSpace.INITIAL) { + terminationCause = TerminationCause.forException(new ConnectException(msg)); + } else { + terminationCause = TerminationCause.forException(new IOException(msg)); + } + terminator.terminate(terminationCause); + } else { + if (debug.on()) { + debug.log("%s: Max PTO backoff reached (%s) before min probe timeout exceeded (%s)," + + " allow more backoff %s", + packetNumberSpace, backoff, MIN_PTO_BACKOFF_TIMEOUT, rttEstimator.state()); + } + if (Log.quicRetransmit() || Log.quicCC()) { + Log.logQuic("%s OUT: %s: Max PTO backoff reached (%s) before min probe timeout exceeded (%s) - %s" + .formatted(QuicConnectionImpl.this.logTag(), packetNumberSpace, backoff, + MIN_PTO_BACKOFF_TIMEOUT, rttEstimator.state())); + } + } + } + } + + // this method is called when a packet has been acknowledged + private void packetAcknowledged(QuicPacket packet) { + // process packet frames to track acknowledgement + // of RESET_STREAM frames etc... + if (debug.on()) { + debug.log("Packet %s(pn:%s) is acknowledged by peer", + packet.packetType(), + packet.packetNumber()); + } + packet.frames().forEach(this::frameAcknowledged); + } + + // this method is called when a frame has been acknowledged + private void frameAcknowledged(QuicFrame frame) { + if (frame instanceof ResetStreamFrame reset) { + long streamId = reset.streamId(); + if (streams.isSendingStream(streamId)) { + streams.streamResetAcknowledged(reset); + } + } else if (frame instanceof StreamFrame streamFrame) { + if (streamFrame.isLast()) { + streams.streamDataSentAcknowledged(streamFrame); + } + } + } + + protected PacketSpaces packetNumberSpaces() { + return packetSpaces; + } + protected PacketSpace packetSpace(PacketNumberSpace packetNumberSpace) { + return packetSpaces.get(packetNumberSpace); + } + + public String dbgTag() { return dbgTag; } + + public String streamDbgTag(long streamId, String direction) { + String dir = direction == null || direction.isEmpty() + ? "" : ("(" + direction + ")"); + return dbgTag + "[streamId" + dir + "=" + streamId + "]"; + } + + + @Override + public CompletableFuture openNewLocalBidiStream(final Duration limitIncreaseDuration) { + if (!stateHandle.opened()) { + return MinimalFuture.failedFuture(new ClosedChannelException()); + } + final CompletableFuture> streamCF = + this.handshakeFlow.handshakeCF().thenApply((ignored) -> + streams.createNewLocalBidiStream(limitIncreaseDuration)); + return streamCF.thenCompose(Function.identity()); + } + + @Override + public CompletableFuture openNewLocalUniStream(final Duration limitIncreaseDuration) { + if (!stateHandle.opened()) { + return MinimalFuture.failedFuture(new ClosedChannelException()); + } + final CompletableFuture> streamCF = + this.handshakeFlow.handshakeCF().thenApply((ignored) + -> streams.createNewLocalUniStream(limitIncreaseDuration)); + return streamCF.thenCompose(Function.identity()); + } + + @Override + public void addRemoteStreamListener(Predicate streamConsumer) { + streams.addRemoteStreamListener(streamConsumer); + } + + @Override + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + return streams.removeRemoteStreamListener(streamConsumer); + } + + @Override + public Stream quicStreams() { + return streams.quicStreams(); + } + + @Override + public List connectionIds() { + return localConnIdManager.connectionIds(); + } + + LocalConnIdManager localConnectionIdManager() { + return localConnIdManager; + } + + /** + * {@return the local connection id} + */ + public QuicConnectionId localConnectionId() { + return connectionId; + } + + /** + * {@return the peer connection id} + */ + public QuicConnectionId peerConnectionId() { + return this.peerConnIdManager.getPeerConnId(); + } + + /** + * Returns the original connection id. + * This is the original destination connection id that + * the client generated when connecting to the server for + * the first time. + * @return the original connection id + */ + protected QuicConnectionId originalServerConnId() { + return this.peerConnIdManager.originalServerConnId(); + } + + private record IncomingDatagram(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) {} + + @Override + public boolean accepts(SocketAddress source) { + // The client ever accepts packets from two sources: + // => the original peer address + // => the preferred peer address (not implemented) + if (!source.equals(peerAddress)) { + // We only accept packets from the endpoint to + // which we send them. + if (debug.on()) { + debug.log("unexpected sender %s, skipping packet", source); + } + return false; + } + return true; + } + + public void processIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) { + // Processes an incoming datagram that has just been + // read off the network. + if (debug.on()) { + debug.log("processIncoming %s(pos=%d, remaining=%d)", + headersType, buffer.position(), buffer.remaining()); + } + if (!stateHandle.opened()) { + if (debug.on()) { + debug.log("connection closed, skipping packet"); + } + return; + } + + assert accepts(source); + + scheduleForDecryption(new IncomingDatagram(source, destConnId, headersType, buffer)); + } + + public void internalProcessIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) { + try { + int packetIndex = 0; + while(buffer.hasRemaining()) { + int startPos = buffer.position(); + packetIndex++; + boolean isLongHeader = QuicPacketDecoder.peekHeaderType(buffer, startPos) == QuicPacket.HeadersType.LONG; + // It's only safe to check version here if versionNegotiated is true. + // We might be receiving an INITIAL packet before the version negotiation + // has been handled. + if (isLongHeader) { + LongHeader header = QuicPacketDecoder.peekLongHeader(buffer); + if (header == null) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram): too short", packetIndex); + } + return; + } + if (!header.destinationId().matches(destConnId)) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram):" + + " wrong connection id (%s vs %s)", + packetIndex, + header.destinationId().toHexString(), + Utils.asHexString(destConnId)); + } + return; + } + var peekedVersion = header.version(); + final var version = this.quicVersion.versionNumber(); + if (version != peekedVersion) { + if (peekedVersion == 0) { + if (!versionCompatible) { + VersionNegotiationPacket packet = (VersionNegotiationPacket) codingContext.parsePacket(buffer); + processDecrypted(packet); + } else { + if (debug.on()) { + debug.log("Versions packet (%s in datagram) ignored", packetIndex); + } + } + return; + } + QuicVersion packetVersion = QuicVersion.of(peekedVersion).orElse(null); + if (packetVersion == null) { + if (debug.on()) { + debug.log("Unknown Quic version in long header packet" + + " (%s in datagram) %s: 0x%x", + packetIndex, headersType, peekedVersion); + } + return; + } else if (versionNegotiated) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram)" + + " with version %s, already negotiated %s", + packetIndex, packetVersion, quicVersion); + } + return; + } else if (!quicInstance().isVersionAvailable(packetVersion)) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram)" + + " with disabled version %s", + packetIndex, packetVersion); + } + return; + } else { + // do we need to be less trusting here? + if (debug.on()) { + debug.log("Switching version to %s, previous: %s", + packetVersion, quicVersion); + } + switchVersion(packetVersion); + } + } + if (decoder.peekPacketType(buffer) == PacketType.INITIAL && + !quicTLSEngine.keysAvailable(KeySpace.INITIAL)) { + if (debug.on()) { + debug.log("Dropping INITIAL packet (%s in datagram): %s", + packetIndex, "keys discarded"); + } + decoder.skipPacket(buffer, startPos); + continue; + } + } else { + var cid = QuicPacketDecoder.peekShortConnectionId(buffer, destConnId.remaining()); + if (cid == null) { + if (debug.on()) { + debug.log("Dropping short header packet (%s in datagram):" + + " too short", packetIndex); + } + return; + } + if (cid.mismatch(destConnId) != -1) { + if (debug.on()) { + debug.log("Dropping short header packet (%s in datagram):" + + " wrong connection id (%s vs %s)", + packetIndex, Utils.asHexString(cid), Utils.asHexString(destConnId)); + } + return; + } + + } + ByteBuffer packet = decoder.nextPacketSlice(buffer, buffer.position()); + PacketType packetType = decoder.peekPacketType(packet); + if (debug.on()) { + debug.log("unprotecting packet (%s in datagram) %s(%s bytes)", + packetIndex, packetType, packet.remaining()); + } + decrypt(packet); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to process incoming packet", t); + } + } + } + + /** + * Called when an incoming packet has been decrypted. + *

    + * @param quicPacket the decrypted quic packet + */ + public void processDecrypted(QuicPacket quicPacket) { + PacketType packetType = quicPacket.packetType(); + long packetNumber = quicPacket.packetNumber(); + if (debug.on()) { + debug.log("processDecrypted %s(%d)", packetType, packetNumber); + } + if (Log.quicPacketInLoggable(quicPacket)) { + Log.logQuicPacketIn(logTag(), quicPacket); + } + if (packetType != PacketType.VERSIONS) { + versionCompatible = true; + // versions will also set versionCompatible later + } + if (isClientConnection() + && quicPacket instanceof InitialPacket longPacket + && quicPacket.frames().stream().anyMatch(CryptoFrame.class::isInstance)) { + markVersionNegotiated(longPacket.version()); + } + PacketSpace packetSpace = null; + if (packetNumber >= 0) { + packetSpace = packetSpace(quicPacket.numberSpace()); + + // From RFC 9000, Section 13.2.3: + // A receiver MUST retain an ACK Range unless it can ensure that + // it will not subsequently accept packets with numbers in + // that range. Maintaining a minimum packet number that increases + // as ranges are discarded is one way to achieve this with minimal + // state. + long threshold = packetSpace.getMinPNThreshold(); + if (packetNumber <= threshold) { + // discard the packet, as we are no longer acknowledging + // packets in this range. + if (debug.on()) + debug.log("discarding packet %s(%d) - threshold: %d", + packetType, packetNumber, threshold); + return; + } + if (packetSpace.isAcknowledged(packetNumber)) { + if (debug.on()) + debug.log("discarding packet %s(%d) - duplicated", + packetType, packetNumber, threshold); + } + + if (debug.on()) { + debug.log("receiving packet %s(pn:%s, %s)", packetType, + packetNumber, quicPacket.frames()); + } + } + switch (packetType) { + case VERSIONS -> processVersionNegotiationPacket(quicPacket); + case INITIAL -> processInitialPacket(quicPacket); + case ONERTT -> processOneRTTPacket(quicPacket); + case HANDSHAKE -> processHandshakePacket(quicPacket); + case RETRY -> processRetryPacket(quicPacket); + case ZERORTT -> { + if (debug.on()) { + debug.log("Dropping unhandled quic packet %s", packetType); + } + } + case NONE -> throw new InternalError("Unrecognized packet type"); + } + // packet has been processed successfully - connection isn't idle (RFC-9000, section 10.1) + this.terminator.keepAlive(); + if (packetSpace != null) { + packetSpace.packetReceived( + packetType, + packetNumber, + quicPacket.isAckEliciting()); + } + } + + /** + * {@return true if this is a stream initiated locally, and false if + * this is a stream initiated by the peer}. + * @param streamId a stream ID. + */ + protected final boolean isLocalStream(long streamId) { + return isClientConnection() == QuicStreams.isClientInitiated(streamId); + } + + /** + * If a stream with this streamId was already created, returns it. + * @param streamId the stream ID + * @return the stream identified by the given {@code streamId}, or {@code null}. + */ + protected QuicStream findStream(long streamId) { + return streams.findStream(streamId); + } + + /** + * @return true if this stream ID identifies a stream that was + * already opened + * @param streamId the stream id + */ + protected boolean isExistingStreamId(long streamId) { + long next = streams.peekNextStreamId(streamType(streamId)); + return streamId < next; + } + + /** + * Get or open a peer initiated stream with the given stream ID + * @param streamId the id of the remote stream + * @param frameType type of the frame received, used in exceptions + * @return the remote initiated stream identified by the given + * stream ID, or null + * @throws QuicTransportException if the streamID is higher than allowed + */ + protected QuicStream openOrGetRemoteStream(long streamId, long frameType) throws QuicTransportException { + assert !isLocalStream(streamId); + return streams.getOrCreateRemoteStream(streamId, frameType); + } + + /** + * Called to process a {@link OneRttPacket} after it has been successfully decrypted + * @param quicPacket the Quic packet + * @throws IllegalArgumentException if the {@code quicPacket} isn't a 1-RTT packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processOneRTTPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.ONERTT) { + throw new IllegalArgumentException("Not a ONERTT packet: " + quicPacket.packetType()); + } + assert quicPacket instanceof OneRttPacket : "Unexpected ONERTT packet class type: " + + quicPacket.getClass(); + final OneRttPacket oneRTT = (OneRttPacket) quicPacket; + try { + if (debug.on()) { + debug.log("processing packet ONERTT(%s)", quicPacket.packetNumber()); + } + final var frames = oneRTT.frames(); + if (debug.on()) { + debug.log("processing frames: " + frames.stream() + .map(Object::getClass).map(Class::getSimpleName) + .collect(Collectors.joining(", ", "[", "]"))); + } + for (var frame : oneRTT.frames()) { + if (!frame.isValidIn(PacketType.ONERTT)) { + throw new QuicTransportException("Invalid frame in ONERTT packet", + KeySpace.ONE_RTT, frame.getTypeField(), + PROTOCOL_VIOLATION); + } + if (debug.on()) { + debug.log("received 1-RTT frame %s", frame); + } + switch (frame) { + case AckFrame ackFrame -> { + incoming1RTTFrame(ackFrame); + } + case StreamFrame streamFrame -> { + incoming1RTTFrame(streamFrame); + } + case CryptoFrame crypto -> { + incoming1RTTFrame(crypto); + } + case ResetStreamFrame resetStreamFrame -> { + incoming1RTTFrame(resetStreamFrame); + } + case DataBlockedFrame dataBlockedFrame -> { + incoming1RTTFrame(dataBlockedFrame); + } + case StreamDataBlockedFrame streamDataBlockedFrame -> { + incoming1RTTFrame(streamDataBlockedFrame); + } + case StreamsBlockedFrame streamsBlockedFrame -> { + incoming1RTTFrame(streamsBlockedFrame); + } + case PaddingFrame paddingFrame -> { + incoming1RTTFrame(paddingFrame); + } + case MaxDataFrame maxData -> { + incoming1RTTFrame(maxData); + } + case MaxStreamDataFrame maxStreamData -> { + incoming1RTTFrame(maxStreamData); + } + case MaxStreamsFrame maxStreamsFrame -> { + incoming1RTTFrame(maxStreamsFrame); + } + case StopSendingFrame stopSendingFrame -> { + incoming1RTTFrame(stopSendingFrame); + } + case PingFrame ping -> { + incoming1RTTFrame(ping); + } + case ConnectionCloseFrame close -> { + incoming1RTTFrame(close); + } + case HandshakeDoneFrame handshakeDoneFrame -> { + incoming1RTTFrame(handshakeDoneFrame); + } + case NewConnectionIDFrame newCid -> { + incoming1RTTFrame(newCid); + } + case RetireConnectionIDFrame retireCid -> { + incoming1RTTFrame(oneRTT, retireCid); + } + case NewTokenFrame newTokenFrame -> { + incoming1RTTFrame(newTokenFrame); + } + case PathResponseFrame pathResponseFrame -> { + incoming1RTTFrame(pathResponseFrame); + } + case PathChallengeFrame pathChallengeFrame -> { + incoming1RTTFrame(pathChallengeFrame); + } + default -> { + if (debug.on()) { + debug.log("Frame type: %s not supported yet", frame.getClass()); + } + } + } + } + } catch (Throwable t) { + onProcessingError(quicPacket, t); + } + } + + /** + * Gets a receiving stream instance for the given ID, used for processing + * incoming STREAM, RESET_STREAM and STREAM_DATA_BLOCKED frames. + * Returns null if the instance is gone already. Throws an exception if the stream ID is incorrect. + * @param streamId stream ID + * @param frameType received frame type. Used in QuicTransportException + * @return receiver stream, or null if stream is already gone + * @throws QuicTransportException if the stream ID is not a valid receiving stream + */ + private QuicReceiverStream getReceivingStream(long streamId, long frameType) throws QuicTransportException { + var stream = findStream(streamId); + boolean isLocalStream = isLocalStream(streamId); + boolean isUnidirectional = isUnidirectional(streamId); + if (isLocalStream && isUnidirectional) { + // stream is write-only + throw new QuicTransportException("Stream %s (type %s) is unidirectional" + .formatted(streamId, streamType(streamId)), + KeySpace.ONE_RTT, frameType, QuicTransportErrors.STREAM_STATE_ERROR); + } + if (stream == null && isLocalStream) { + // the stream is either closed or bad stream + if (!isExistingStreamId(streamId)) { + throw new QuicTransportException("No such stream %s (type %s)" + .formatted(streamId, streamType(streamId)), + KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_STATE_ERROR); + } + return null; + } + + if (stream == null) { + assert !isLocalStream; + // Note: The quic protocol allows any peer to open + // a bidirectional remote stream. + // The HTTP/3 protocol does not allow a server to open a + // bidirectional stream on the client. If this is a client + // connection and the stream type is bidirectional and + // remote, the connection will be closed by the HTTP/3 + // higher level protocol but not here, since this is + // not a Quic protocol error. + stream = openOrGetRemoteStream(streamId, frameType); + if (stream == null) { + return null; + } + } + return (QuicReceiverStream)stream; + } + + /** + * Gets a sending stream instance for the given ID, used for processing + * incoming MAX_STREAM_DATA and STOP_SENDING frames. + * Returns null if the instance is gone already. Throws an exception if the stream ID is incorrect. + * @param streamId stream ID + * @param frameType received frame type. Used in QuicTransportException + * @return sender stream, or null if stream is already gone + * @throws QuicTransportException if the stream ID is not a valid sending stream + */ + private QuicSenderStream getSendingStream(long streamId, long frameType) throws QuicTransportException { + var stream = findStream(streamId); + boolean isLocalStream = isLocalStream(streamId); + boolean isUnidirectional = isUnidirectional(streamId); + if (!isLocalStream && isUnidirectional) { + // stream is read-only + throw new QuicTransportException("Stream %s (type %s) is unidirectional" + .formatted(streamId, streamType(streamId)), + QuicTLSEngine.KeySpace.ONE_RTT, frameType, QuicTransportErrors.STREAM_STATE_ERROR); + } + if (stream == null && isLocalStream) { + // the stream is either closed or bad stream + if (!isExistingStreamId(streamId)) { + throw new QuicTransportException("No such stream %s (type %s)" + .formatted(streamId, streamType(streamId)), + QuicTLSEngine.KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_STATE_ERROR); + } + return null; + } + + if (stream == null) { + assert !isLocalStream; + stream = openOrGetRemoteStream(streamId, frameType); + if (stream == null) { + return null; + } + } + return (QuicSenderStream)stream; + } + + /** + * Called to process an {@link InitialPacket} after it has been decrypted. + * @param quicPacket the Quic packet + * @throws IllegalArgumentException if {@code quicPacket} isn't a INITIAL packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processInitialPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.INITIAL) { + throw new IllegalArgumentException("Not a INITIAL packet: " + quicPacket.packetType()); + } + try { + if (quicPacket instanceof InitialPacket initial) { + MaxInitialTimer initialTimer = this.maxInitialTimer; + if (initialTimer != null) { + // will be a no-op after the first call; + initialTimer.initialPacketReceived(); + // we no longer need the timer + this.maxInitialTimer = null; + } + int total; + updatePeerConnectionId(initial); + total = processInitialPacketPayload(initial); + assert total == initial.payloadSize(); + // received initial packet from server - we won't need to replay anything now + handshakeFlow.localInitial.discardReplayData(); + continueHandshake(); + if (quicTLSEngine.getHandshakeState() == HandshakeState.NEED_RECV_CRYPTO && + quicTLSEngine.keysAvailable(KeySpace.HANDSHAKE)) { + // arm the anti-deadlock PTO timer + packetSpaces.handshake.runTransmitter(); + } + } else { + throw new InternalError("Bad packet type: " + quicPacket); + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + protected void updatePeerConnectionId(InitialPacket initial) throws QuicTransportException { + this.incomingInitialPacketSourceId = initial.sourceId(); + this.peerConnIdManager.finalizeHandshakePeerConnId(initial); + } + + public QuicConnectionId getIncomingInitialPacketSourceId() { + return incomingInitialPacketSourceId; + } + + @Override + public CompletableFuture handshakeReachedPeer() { + return this.handshakeFlow.handshakeReachedPeerCF; + } + + /** + * Process the payload of an incoming initial packet + * @param packet the incoming packet + * @return the total number of bytes consumed + * @throws SSLHandshakeException if the handshake failed + * @throws IOException if a frame couldn't be decoded, or the payload + * wasn't entirely consumed. + */ + protected int processInitialPacketPayload(final InitialPacket packet) + throws IOException, QuicTransportException { + int provided=0, total=0; + int initialPayloadSize = packet.payloadSize(); + if (debug.on()) { + debug.log("Processing initial packet pn:%s payload:%s", + packet.packetNumber(), initialPayloadSize); + } + for (final var frame: packet.frames()) { + if (debug.on()) { + debug.log("received INITIAL frame %s", frame); + } + int size = frame.size(); + total += size; + switch (frame) { + case AckFrame ack -> { + incomingInitialFrame(ack); + } + case CryptoFrame crypto -> { + provided = incomingInitialFrame(crypto); + } + case PaddingFrame paddingFrame -> { + incomingInitialFrame(paddingFrame); + } + case PingFrame ping -> { + incomingInitialFrame(ping); + } + case ConnectionCloseFrame close -> { + incomingInitialFrame(close); + } + default -> { + if (debug.on()) { + debug.log("Received invalid frame: " + frame); + } + assert !frame.isValidIn(packet.packetType()) : frame.getClass(); + throw new QuicTransportException("Invalid frame in this packet type", + packet.packetType().keySpace().orElse(null), frame.getTypeField(), + PROTOCOL_VIOLATION); + } + } + } + if (total != initialPayloadSize) { + throw new IOException("Initial payload wasn't fully consumed: %s read, of which %s crypto, from %s size" + .formatted(total, provided, initialPayloadSize)); + } + return total; + } + /** + * Process the payload of an incoming handshake packet + * @param packet the incoming packet + * @return the total number of bytes consumed + * @throws SSLHandshakeException if the handshake failed + * @throws IOException if a frame couldn't be decoded, or the payload + * wasn't entirely consumed. + */ + protected int processHandshakePacketPayload(final HandshakePacket packet) + throws IOException, QuicTransportException { + int provided=0, total=0; + int payloadSize = packet.payloadSize(); + for (final var frame: packet.frames()) { + if (debug.on()) { + debug.log("received HANDSHAKE frame %s", frame); + } + int size = frame.size(); + total += size; + switch (frame) { + case AckFrame ack -> { + incomingHandshakeFrame(ack); + } + case CryptoFrame crypto -> { + provided = incomingHandshakeFrame(crypto); + } + case PaddingFrame paddingFrame -> { + incomingHandshakeFrame(paddingFrame); + } + case PingFrame ping -> { + incomingHandshakeFrame(ping); + } + case ConnectionCloseFrame close -> { + incomingHandshakeFrame(close); + } + default -> { + assert !frame.isValidIn(packet.packetType()) : frame.getClass(); + throw new QuicTransportException("Invalid frame in this packet type", + packet.packetType().keySpace().orElse(null), frame.getTypeField(), + PROTOCOL_VIOLATION); + } + } + } + if (total != payloadSize) { + throw new IOException("Handshake payload wasn't fully consumed: %s read, of which %s crypto, from %s size" + .formatted(total, provided, payloadSize)); + } + return total; + } + + /** + * Called to process an {@link HandshakePacket} after it has been decrypted. + * @param quicPacket the handshake quic packet + * @throws IllegalArgumentException if {@code quicPacket} is not a HANDSHAKE packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processHandshakePacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.HANDSHAKE) { + throw new IllegalArgumentException("Not a HANDSHAKE packet: " + quicPacket.packetType()); + } + final var handshake = this.handshakeFlow.handshakeCF(); + if (handshake.isDone() && debug.on()) { + debug.log("Receiving HandshakePacket(%s) after handshake is done: %s", + quicPacket.packetNumber(), quicPacket.frames()); + } + try { + if (quicPacket instanceof HandshakePacket hs) { + int total; + total = processHandshakePacketPayload(hs); + assert total == hs.payloadSize(); + continueHandshake(); + } else { + throw new InternalError("Bad packet type: " + quicPacket); + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * Called to process a {@link RetryPacket} after it has been decrypted. + * @param quicPacket the retry quic packet + * @throws IllegalArgumentException if {@code quicPacket} is not a RETRY packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processRetryPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.RETRY) { + throw new IllegalArgumentException("Not a RETRY packet: " + quicPacket.packetType()); + } + try { + if (!(quicPacket instanceof RetryPacket rt)) { + throw new InternalError("Bad packet type: " + quicPacket); + } + assert stateHandle.helloSent() : "unexpected message"; + if (rt.retryToken().length == 0) { + if (debug.on()) { + debug.log("Invalid retry, empty token"); + } + return; + } + final QuicConnectionId currentPeerConnId = this.peerConnIdManager.getPeerConnId(); + if (rt.sourceId().equals(currentPeerConnId)) { + if (debug.on()) { + debug.log("Invalid retry, same connection ID"); + } + return; + } + if (this.peerConnIdManager.retryConnId() != null) { + if (debug.on()) { + debug.log("Ignoring retry, already got one"); + } + return; + } + // ignore retry if we already received initial packets + if (incomingInitialPacketSourceId != null) { + if (debug.on()) { + debug.log("Already received initial, ignoring retry"); + } + return; + } + final int version = rt.version(); + final QuicVersion retryVersion = QuicVersion.of(version).orElse(null); + if (retryVersion == null) { + if (debug.on()) { + debug.log("Ignoring retry packet with unknown version 0x" + + Integer.toHexString(version)); + } + // ignore the packet + return; + } + final QuicVersion originalVersion = this.quicVersion; // the original version used to establish the connection + if (originalVersion != retryVersion) { + if (debug.on()) { + debug.log("Ignoring retry packet with version 0x" + + Integer.toHexString(version) + + " since it doesn't match the original version 0x" + + Integer.toHexString(originalVersion.versionNumber())); + } + // ignore the packet + return; + } + ReentrantLock tl = packetSpaces.initial.getTransmitLock(); + tl.lock(); + try { + initialToken = rt.retryToken(); + final QuicConnectionId retryConnId = rt.sourceId(); + this.peerConnIdManager.retryConnId(retryConnId); + quicTLSEngine.deriveInitialKeys(originalVersion, retryConnId.asReadOnlyBuffer()); + this.packetSpace(PacketNumberSpace.INITIAL).retry(); + handshakeFlow.localInitial.replayData(); + } finally { + tl.unlock(); + } + packetSpaces.initial.runTransmitter(); + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * {@return the next (higher) max streams limit that should be advertised to the remote peer. + * Returns {@code 0} if the limit should not be increased} + * + * @param bidi true if bidirectional stream, false otherwise + */ + public long nextMaxStreamsLimit(final boolean bidi) { + if (isClientConnection() && bidi) return 0; // server does not open bidi streams + return streams.nextMaxStreamsLimit(bidi); + } + + /** + * Called when a stateless reset token is received. + */ + @Override + public void processStatelessReset() { + terminator.incomingStatelessReset(); + } + + /** + * Called to process a received {@link VersionNegotiationPacket} + * @param quicPacket the {@link VersionNegotiationPacket} + * @throws IllegalArgumentException if {@code quicPacket} is not a {@link PacketType#VERSIONS} + * packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processVersionNegotiationPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.VERSIONS) { + throw new IllegalArgumentException("Not a VERSIONS packet type: " + quicPacket.packetType()); + } + // servers aren't expected to receive version negotiation packet + if (!this.isClientConnection()) { + if (debug.on()) { + debug.log("(server) ignoring version negotiation packet"); + } + return; + } + try { + final var handshakeCF = this.handshakeFlow.handshakeCF(); + // we must ignore version negotiation if we already had a successful exchange + var versionCompatible = this.versionCompatible; + if (versionCompatible || handshakeCF.isDone()) { + if (debug.on()) { + debug.log("ignoring version negotiation packet (neg: %s, state: %s, hs: %s)", + versionCompatible, stateHandle, handshakeCF); + } + return; + } + // we shouldn't receive unsolicited version negotiation packets + assert stateHandle.helloSent(); + if (!(quicPacket instanceof VersionNegotiationPacket negotiate)) { + if (debug.on()) { + debug.log("Bad packet type %s for %s", + quicPacket.getClass().getName(), quicPacket); + } + return; + } + if (!negotiate.sourceId().equals(originalServerConnId())) { + if (debug.on()) { + debug.log("Received version negotiation packet with wrong connection id"); + debug.log("expected source id: %s, received source id: %s", + originalServerConnId(), negotiate.sourceId()); + debug.log("ignoring version negotiation packet (wrong id)"); + } + return; + } + final int[] serverSupportedVersions = negotiate.supportedVersions(); + if (debug.on()) { + debug.log("Received version negotiation packet with supported=%s", + Arrays.toString(serverSupportedVersions)); + } + assert this.quicInstance() instanceof QuicClient : "Not a quic client"; + final QuicClient client = (QuicClient) this.quicInstance(); + QuicVersion negotiatedVersion = null; + for (final int v : serverSupportedVersions) { + final QuicVersion serverVersion = QuicVersion.of(v).orElse(null); + if (serverVersion == null) { + if (debug.on()) { + debug.log("Ignoring unrecognized server supported version %d", v); + } + continue; + } + if (serverVersion == this.quicVersion) { + // RFC-9000, section 6.2: + // A client MUST discard a Version Negotiation packet that lists + // the QUIC version selected by the client. + if (debug.on()) { + debug.log("ignoring version negotiation packet since the version" + + " %d matches the current quic version selected by the client", v); + } + return; + } + // check if the current quic client is enabled for this version + if (!client.isVersionAvailable(serverVersion)) { + if (debug.on()) { + debug.log("Ignoring server supported version %d because the " + + "client isn't enabled for it", v); + } + continue; + } + if (debug.on()) { + if (negotiatedVersion == null) { + debug.log("Accepting server supported version %d", + serverVersion.versionNumber()); + negotiatedVersion = serverVersion; + } else { + // currently all versions are equal + debug.log("Skipping server supported version %d", + serverVersion.versionNumber()); + } + } + } + // at this point if negotiatedVersion is null, then it implies that none of the server + // supported versions are supported by the client. The spec expects us to abandon the + // current connection attempt in such cases (RFC-9000, section 6.2) + if (negotiatedVersion == null) { + final String msg = "No support for any of the QUIC versions being negotiated: " + + Arrays.toString(serverSupportedVersions); + if (debug.on()) { + debug.log("No version could be negotiated: %s", msg); + } + terminator.terminate(forException(new IOException(msg))); + return; + } + // a different version than the current client chosen version has been negotiated, + // switch the client connection to use this negotiated version + ReentrantLock tl = packetSpaces.initial.getTransmitLock(); + tl.lock(); + try { + if (switchVersion(negotiatedVersion)) { + final ByteBuffer quicInitialParameters = buildInitialParameters(); + quicTLSEngine.setLocalQuicTransportParameters(quicInitialParameters); + quicTLSEngine.restartHandshake(); + handshakeFlow.localInitial.reset(); + continueHandshake(); + packetSpaces.initial.runTransmitter(); + this.versionCompatible = true; + processedVersionsPacket = true; + } + } finally { + tl.unlock(); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to handle packet", t); + } + } + + } + + /** + * Switch to a new version after receiving a version negotiation + * packet. This method checks that no version was previously + * negotiated, in which case it switches the connection to the + * new version and returns true. + * Otherwise, it returns false. + * + * @param negotiated the new version that was negotiated + * @return true if switching to the new version was successful + */ + protected boolean switchVersion(QuicVersion negotiated) { + try { + assert !versionNegotiated; + if (debug.on()) + debug.log("switch to negotiated version %s", negotiated); + this.quicVersion = negotiated; + this.decoder = QuicPacketDecoder.of(negotiated); + this.encoder = QuicPacketEncoder.of(negotiated); + this.packetSpace(PacketNumberSpace.INITIAL).versionChanged(); + // regenerate the INITIAL keys using the new negotiated Quic version + this.quicTLSEngine.deriveInitialKeys(negotiated, originalServerConnId().asReadOnlyBuffer()); + return true; + } catch (Throwable t) { + terminator.terminate(forException(t)); + throw new RuntimeException("failed to switch to version", t); + } + } + + /** + * Mark the version as negotiated. No further version changes are possible. + * + * @param packetVersion the packet version + */ + protected void markVersionNegotiated(int packetVersion) { + int version = this.quicVersion.versionNumber(); + assert packetVersion == version; + if (!versionNegotiated) { + if (VERSION_NEGOTIATED.compareAndSet(this, false, true)) { + // negotiated version finalized + quicTLSEngine.versionNegotiated(QuicVersion.of(version).get()); + } + } + } + + /** + * {@return a boolean value telling whether the datagram in the + * protection record is complete} + * The datagram is complete when no other packet need to be coalesced + * in the datagram. + * If a datagram is complete, it is ready to be sent. + * + * @param protectionRecord the protection record + */ + protected boolean isDatagramComplete(ProtectionRecord protectionRecord) { + return protectionRecord.datagram.remaining() == 0 + || protectionRecord.flags == ProtectionRecord.SINGLE_PACKET + || (protectionRecord.flags & ProtectionRecord.LAST_PACKET) != 0 + || (protectionRecord.flags & ProtectionRecord.COALESCED) == 0; + } + + /** + * {@return the peer address that should be used when sending datagram + * to the peer} + */ + public InetSocketAddress peerAddress() { + return peerAddress; + } + + /** + * {@return the local address of the quic endpoint} + * @throws UncheckedIOException if the address is not available + */ + public SocketAddress localAddress() { + try { + var endpoint = this.endpoint; + if (endpoint == null) { + throw new IOException("no endpoint defined"); + } + return endpoint.getLocalAddress(); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + /** + * Pushes the {@linkplain ProtectionRecord#datagram() datagram} contained in + * the {@code protectionRecord}, through the {@linkplain QuicEndpoint endpoint}. + * + * @param protectionRecord the ProtectionRecord containing the datagram + */ + private void pushEncryptedDatagram(final ProtectionRecord protectionRecord) { + final long packetNumber = protectionRecord.packet().packetNumber(); + assert packetNumber >= 0 : "unexpected packet number: " + packetNumber; + final long retransmittedPacketNumber = protectionRecord.retransmittedPacketNumber(); + assert packetNumber > retransmittedPacketNumber : "packet number: " + packetNumber + + " was expected to be greater than packet the packet being retransmitted: " + + retransmittedPacketNumber; + final boolean pktContainsConnClose = containsConnectionClose(protectionRecord.packet()); + // if the connection isn't open then except for the packet containing a CONNECTION_CLOSE + // frame, we don't push any other packets. + if (!isOpen() && !pktContainsConnClose) { + if (debug.on()) { + debug.log("connection isn't open - ignoring %s(pn:%s): frames:%s", + protectionRecord.packet.packetType(), + protectionRecord.packet.packetNumber(), + protectionRecord.packet.frames()); + } + datagramDropped(new QuicDatagram(this, peerAddress, protectionRecord.datagram)); + return; + } + // TODO: revisit this: we need to figure out how best to emit coalesced packet, + // and having one protection record per packet may not be the the best. + // Maybe a protection record should have a list of coalesced packets + // instead of a single packet? + final ByteBuffer datagram = protectionRecord.datagram(); + final int firstPacketOffset = protectionRecord.firstPacketOffset(); + // flip the datagram + datagram.limit(datagram.position()); + datagram.position(firstPacketOffset); + if (debug.on()) { + final PacketType packetType = protectionRecord.packet().packetType(); + final int packetOffset = protectionRecord.packetOffset(); + if (packetOffset == firstPacketOffset) { + debug.log("Pushing datagram([%s(%d)], %d)", packetType, packetNumber, + datagram.remaining()); + } else { + debug.log("Pushing coalesced datagram([%s(%d)], %d)", + packetType, packetNumber, datagram.remaining()); + } + } + + // upon successful sending of the datagram, notify that the packet was sent + // we call packetSent just before sending the packet here, to make sure + // that the PendingAcknowledgement will be present in the queue before + // we receive the ACK frame from the server. Not doing this would create + // a race where the peer might be able to send the ack, and we might process + // it, before the PendingAcknowledgement is added. + final QuicPacket packet = protectionRecord.packet(); + final PacketSpace packetSpace = packetSpace(packet.numberSpace()); + packetSpace.packetSent(packet, retransmittedPacketNumber, packetNumber); + + // if we are sending a packet containing a CONNECTION_CLOSE frame, then we + // also switch/remove the current connection instance in the endpoint. + if (pktContainsConnClose) { + if (stateHandle.isMarked(QuicConnectionState.DRAINING)) { + // a CONNECTION_CLOSE frame is being sent to the peer when the local + // connection state is in DRAINING. This implies that the local endpoint + // is responding to an incoming CONNECTION_CLOSE frame from the peer. + // we remove the connection from the endpoint for such cases. + endpoint.pushClosedDatagram(this, peerAddress(), datagram); + } else if (stateHandle.isMarked(QuicConnectionState.CLOSING)) { + // a CONNECTION_CLOSE frame is being sent to the peer when the local + // connection state is in CLOSING. For such cases, we switch this + // connection in the endpoint to one which responds with + // CONNECTION_CLOSE frame for any subsequent incoming packets + // from the peer. + endpoint.pushClosingDatagram(this, peerAddress(), datagram); + } else { + // should not happen + throw new IllegalStateException("connection is neither draining nor closing," + + " cannot send a connection close frame"); + } + } else { + pushDatagram(peerAddress(), datagram); + } + // RFC-9000, section 10.1: An endpoint also restarts its idle timer when sending + // an ack-eliciting packet ... + if (packet.isAckEliciting()) { + this.terminator.keepAlive(); + } + } + + /** + * Calls the {@link QuicEndpoint#pushDatagram(QuicPacketReceiver, SocketAddress, ByteBuffer)} + * + * @param destination The destination of this datagram + * @param datagram The datagram + */ + protected void pushDatagram(final SocketAddress destination, final ByteBuffer datagram) { + endpoint.pushDatagram(this, destination, datagram); + } + + /** + * Called when a datagram scheduled for writing by this connection + * could not be written to the network. + * @param t the error that occurred + */ + @Override + public void onWriteError(Throwable t) { + // log exception if still opened + if (stateHandle.opened()) { + if (Log.errors()) { + Log.logError("%s: Failed to write datagram: %s", dbgTag(), t ); + Log.logError(t); + } else if (debug.on()) { + debug.log("Failed to write datagram", t); + } + } + } + + /** + * Called when a packet couldn't be processed + * @param t the error that occurred + */ + public void onProcessingError(QuicPacket packet, Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + + /** + * Starts the Quic Handshake. + * @return A completable future which will be completed when the + * handshake is completed. + * @throws UnsupportedOperationException If this connection isn't a client connection + */ + public final CompletableFuture startHandshake() { + if (!isClientConnection()) { + throw new UnsupportedOperationException("Not a client connection, cannot start handshake"); + } + if (!this.startHandshakeCalled.compareAndSet(false, true)) { + throw new IllegalStateException("handshake has already been started on connection"); + } + if (this.peerAddress.isUnresolved()) { + // fail if address is unresolved + return MinimalFuture.failedFuture( + Utils.toConnectException(new UnresolvedAddressException())); + } + CompletableFuture cf; + try { + // register the connection with an endpoint + assert this.quicInstance instanceof QuicClient : "Not a QuicClient"; + endpoint.registerNewConnection(this); + cf = MinimalFuture.completedFuture(null); + } catch (Throwable t) { + cf = MinimalFuture.failedFuture(t); + } + return cf.thenApply(this::sendFirstInitialPacket) + .exceptionally((t) -> { + // complete the handshake CFs with the failure + handshakeFlow.failHandshakeCFs(t); + return handshakeFlow; + }) + .thenCompose(HandshakeFlow::handshakeCF) + .thenApply(this::onHandshakeCompletion); + } + + /** + * This method is called when the handshake is successfully completed. + * @param result the result of the handshake + */ + protected QuicEndpoint onHandshakeCompletion(final HandshakeState result) { + if (debug.on()) { + debug.log("Quic handshake successfully completed with %s(%s)", + quicTLSEngine.getApplicationProtocol(), peerAddress()); + } + // now that the handshake has successfully completed, start the + // idle timeout management for this connection + this.idleTimeoutManager.start(); + return this.endpoint; + } + + protected HandshakeFlow handshakeFlow() { + return handshakeFlow; + } + + protected void startInitialTimer() { + if (!isClientConnection()) return; + MaxInitialTimer initialTimer = maxInitialTimer; + if (initialTimer == null && DEFAULT_MAX_INITIAL_TIMEOUT < Integer.MAX_VALUE) { + Deadline maxInitialDeadline = null; + synchronized (this) { + initialTimer = maxInitialTimer; + if (initialTimer == null) { + Deadline now = this.endpoint().timeSource().instant(); + maxInitialDeadline = now.plusSeconds(DEFAULT_MAX_INITIAL_TIMEOUT); + initialTimer = maxInitialTimer = new MaxInitialTimer(this.endpoint().timer(), maxInitialDeadline); + } + } + if (maxInitialDeadline != null) { + if (Log.quic()) { + Log.logQuic("{0}: Arming quic initial timer for {1}", logTag(), + Deadline.between(this.endpoint().timeSource().instant(), maxInitialDeadline)); + } + if (debug.on()) { + debug.log("Arming quic initial timer for %s seconds", + Deadline.between(this.endpoint().timeSource().instant(), maxInitialDeadline).toSeconds()); + } + initialTimer.timerQueue.reschedule(initialTimer, maxInitialDeadline); + } + } + } + + // adaptation to Function + private HandshakeFlow sendFirstInitialPacket(Void unused) { + // may happen if connection cancelled before endpoint is + // created + final TerminationCause tc = terminationCause(); + if (tc != null) { + throw new CompletionException(tc.getCloseCause()); + } + try { + startInitialTimer(); + if (Log.quic()) { + Log.logQuic(logTag() + ": connectionId: " + + connectionId.toHexString() + + ", " + endpoint + ": " + endpoint.name() + + " - " + endpoint.getLocalAddressString()); + } else if (debug.on()) { + debug.log(logTag() + ": connectionId: " + + connectionId.toHexString() + + ", " + endpoint + ": " + endpoint.name() + + " - " + endpoint.getLocalAddressString()); + } + var localAddress = endpoint.getLocalAddress(); + var conflict = Utils.addressConflict(localAddress, peerAddress); + if (conflict != null) { + String msg = conflict; + if (debug.on()) { + debug.log("%s (local: %s, remote: %s)", msg, localAddress, peerAddress); + } + Log.logError("{0} {1} (local: {2}, remote: {3})", logTag(), + msg, localAddress, peerAddress); + throw new SSLHandshakeException(msg); + } + final QuicConnectionId clientSelectedPeerId = initialServerConnectionId(); + this.peerConnIdManager.originalServerConnId(clientSelectedPeerId); + handshakeFlow.markHandshakeStart(); + stateHandle.markHelloSent(); + // the "original version" used to establish the connection + final QuicVersion originalVersion = this.quicVersion; + quicTLSEngine.deriveInitialKeys(originalVersion, clientSelectedPeerId.asReadOnlyBuffer()); + final ByteBuffer quicInitialParameters = buildInitialParameters(); + quicTLSEngine.setLocalQuicTransportParameters(quicInitialParameters); + handshakeFlow.localInitial.keepReplayData(); + continueHandshake(); + packetSpaces.initial.runTransmitter(); + } catch (Throwable t) { + terminator.terminate(forException(t)); + throw new CompletionException(terminationCause().getCloseCause()); + } + return handshakeFlow; + } + + private static final Random RANDOM = new SecureRandom(); + + private QuicConnectionId initialServerConnectionId() { + byte[] bytes = new byte[INITIAL_SERVER_CONNECTION_ID_LENGTH]; + RANDOM.nextBytes(bytes); + return new PeerConnectionId(bytes); + } + + /** + * Compose a list of Quic frames containing a crypto frame and an ack frame, + * omitting null frames. + * @param crypto the crypto frame + * @param ack the ack frame + * @return A list of {@link QuicFrame}. + */ + private List makeList(CryptoFrame crypto, AckFrame ack) { + List frames = new ArrayList<>(2); + if (crypto != null) { + frames.add(crypto); + } + if (ack != null) { + frames.add(ack); + } + return frames; + } + + /** + * Allocate a {@link ByteBuffer} that can be used to encrypt the + * given packet. + * @param packet the packet to encrypt + * @return a new {@link ByteBuffer} with sufficient space to encrypt + * the given packet. + */ + protected ByteBuffer allocateDatagramForEncryption(QuicPacket packet) { + int size = packet.size(); + if (packet.hasLength()) { // packet can be coalesced + size = Math.max(size, getMaxDatagramSize()); + } + if (size > getMaxDatagramSize()) { + + if (Log.errors()) { + var error = new AssertionError("%s: Size too big: %s > %s".formatted( + logTag(), + size, getMaxDatagramSize())); + Log.logError(logTag() + ": Packet too big: " + packet.prettyPrint()); + Log.logError(error); + } else if (debug.on()) { + var error = new AssertionError("%s: Size too big: %s > %s".formatted( + logTag(), + size, getMaxDatagramSize())); + debug.log("Packet too big: " + packet.prettyPrint()); + debug.log(error); + } + // Revisit: if we implement Path MTU detection, then the max datagram size + // may evolve, increasing or decreasing as the path change. + // In which case - we may want to tune this, down and only + // log an error or warning? + final String errMsg = "Failed to encode packet, too big: " + size; + terminator.terminate(forTransportError(PROTOCOL_VIOLATION).loggedAs(errMsg)); + throw new UncheckedIOException(terminator.getTerminationCause().getCloseCause()); + } + return getOutgoingByteBuffer(size); + } + + /** + * {@return the maximum datagram size that can be used on the + * connection path} + * @implSpec + * Initially this is {@link #DEFAULT_DATAGRAM_SIZE}, but the + * value will then be decided if the peer sends a specific size + * in the transport parameters and the value can further evolve based + * on path MTU. + */ + // this is public for use in tests + public int getMaxDatagramSize() { + // TODO: we should implement path MTU detection, or maybe let + // this be configurable. Sizes of 32256 or 64512 seem to + // be giving much better throughput when downloading. + // large files + return Math.min(maxPeerAdvertisedPayloadSize, pathMTU); + } + + /** + * Retrieves cryptographic messages from TLS engine, enqueues them for sending + * and starts the transmitter. + */ + protected void continueHandshake() { + handshakeScheduler.runOrSchedule(); + } + + protected void continueHandshake0() { + try { + continueHandshake1(); + } catch (Throwable t) { + var flow = handshakeFlow; + flow.handshakeReachedPeerCF.completeExceptionally(t); + flow.handshakeCF.completeExceptionally(t); + } + } + + private void continueHandshake1() throws IOException { + HandshakeFlow flow = handshakeFlow; + // make sure the localInitialQueue is not modified concurrently + // while we are in this loop + boolean handshakeDataAvailable = false; + boolean initialDataAvailable = false; + for (;;) { + var handshakeState = quicTLSEngine.getHandshakeState(); + if (debug.on()) { + debug.log("continueHandshake: state: %s", handshakeState); + } + if (handshakeState == QuicTLSEngine.HandshakeState.NEED_SEND_CRYPTO) { + // buffer next TLS message + KeySpace keySpace = quicTLSEngine.getCurrentSendKeySpace(); + ByteBuffer payloadBuffer; + handshakeLock.lock(); + try { + payloadBuffer = quicTLSEngine.getHandshakeBytes(keySpace); + assert payloadBuffer != null; + assert payloadBuffer.hasRemaining(); + if (keySpace == KeySpace.INITIAL) { + flow.localInitial.enqueue(payloadBuffer); + initialDataAvailable = true; + } else if (keySpace == KeySpace.HANDSHAKE) { + flow.localHandshake.enqueue(payloadBuffer); + handshakeDataAvailable = true; + } + } finally { + handshakeLock.unlock(); + } + + assert payloadBuffer != null; + if (debug.on()) { + debug.log("continueHandshake: buffered %s bytes in %s keyspace", + payloadBuffer.remaining(), keySpace); + } + } else if (handshakeState == QuicTLSEngine.HandshakeState.NEED_TASK) { + quicTLSEngine.getDelegatedTask().run(); + } else { + if (debug.on()) { + debug.log("continueHandshake: nothing to do (state: %s)", handshakeState); + } + if (initialDataAvailable) { + packetSpaces.initial.runTransmitter(); + } + if (handshakeDataAvailable && flow.localInitial.remaining() == 0) { + packetSpaces.handshake.runTransmitter(); + } + return; + } + } + } + + private boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + if (packetNumberSpace != PacketNumberSpace.APPLICATION) { + // This method can be called by two packet spaces: INITIAL and HANDSHAKE. + // We need to lock to make sure that the method is not run concurrently. + handshakeLock.lock(); + try { + return sendInitialOrHandshakeData(packetNumberSpace); + } finally { + handshakeLock.unlock(); + } + } else { + return oneRttSndQueue.send1RTTData(); + } + } + + private boolean sendInitialOrHandshakeData(final PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: Send %s data", logTag(), packetNumberSpace)); + } + final HandshakeFlow flow = handshakeFlow; + final QuicConnectionId peerConnId = peerConnectionId(); + if (packetNumberSpace == PacketNumberSpace.INITIAL && flow.localInitial.remaining() > 0) { + // process buffered initial data + byte[] token = initialToken(); + int tksize = token == null ? 0 : token.length; + PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.INITIAL); + int maxDstIdLength = isClientConnection() ? + MAX_CONNECTION_ID_LENGTH : // reserve space for the id to grow + peerConnId.length(); + int maxSrcIdLength = connectionId.length(); + // compute maxPayloadSize given maxSizeBeforeEncryption + var largestAckedPN = packetSpace.getLargestPeerAckedPN(); + var packetNumber = packetSpace.allocateNextPN(); + int maxPayloadSize = QuicPacketEncoder.computeMaxInitialPayloadSize(codingContext, 4, tksize, + maxSrcIdLength, maxDstIdLength, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + // compute how many bytes were reserved to allow smooth retransmission + // of packets + int reserved = QuicPacketEncoder.computeMaxInitialPayloadSize(codingContext, + computePacketNumberLength(packetNumber, + codingContext.largestAckedPN(PacketNumberSpace.INITIAL)), + tksize, connectionId.length(), peerConnId.length(), + SMALLEST_MAXIMUM_DATAGRAM_SIZE) - maxPayloadSize; + assert reserved >= 0 : "reserved is negative: " + reserved; + if (debug.on()) { + debug.log("reserved %s byte in initial packet", reserved); + } + if (maxPayloadSize < 5) { + // token too long, can't fit a crypto frame in this packet. Abort. + final String msg = "Initial token too large, maxPayload: " + maxPayloadSize; + terminator.terminate(TerminationCause.forException(new IOException(msg))); + return false; + } + AckFrame ackFrame = packetSpace.getNextAckFrame(false, maxPayloadSize); + int ackSize = ackFrame == null ? 0 : ackFrame.size(); + if (debug.on()) { + debug.log("ack frame size: %d", ackSize); + } + + CryptoFrame crypto = flow.localInitial.produceFrame(maxPayloadSize - ackSize); + int cryptoSize = crypto == null ? 0 : crypto.size(); + assert cryptoSize <= maxPayloadSize : cryptoSize - maxPayloadSize; + List frames = makeList(crypto, ackFrame); + + if (debug.on()) { + debug.log("building initial packet: source=%s, dest=%s", + connectionId, peerConnId); + } + OutgoingQuicPacket packet = encoder.newInitialPacket( + connectionId, peerConnId, token, + packetNumber, largestAckedPN, frames, codingContext); + int size = packet.size(); + if (debug.on()) { + debug.log("initial packet size is %d, max is %d", + size, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + } + assert size == SMALLEST_MAXIMUM_DATAGRAM_SIZE : size - SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + stateHandle.markHelloSent(); + if (debug.on()) { + debug.log("protecting initial quic hello packet for %s(%s) - %d bytes", + Arrays.toString(quicTLSEngine.getSSLParameters().getApplicationProtocols()), + peerAddress(), packet.size()); + } + pushDatagram(ProtectionRecord.single(packet, this::allocateDatagramForEncryption)); + if (flow.localHandshake.remaining() > 0) { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: local handshake has remaining, starting HANDSHAKE transmitter", logTag())); + } + packetSpaces.handshake.runTransmitter(); + } + } else if (packetNumberSpace == PacketNumberSpace.HANDSHAKE && flow.localHandshake.remaining() > 0) { + // process buffered handshake data + PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.HANDSHAKE); + AckFrame ackFrame = packetSpace.getNextAckFrame(false); + int ackSize = ackFrame == null ? 0 : ackFrame.size(); + if (debug.on()) { + debug.log("ack frame size: %d", ackSize); + } + // compute maxPayloadSize given maxSizeBeforeEncryption + var largestAckedPN = packetSpace.getLargestPeerAckedPN(); + var packetNumber = packetSpace.allocateNextPN(); + int maxPayloadSize = QuicPacketEncoder.computeMaxHandshakePayloadSize(codingContext, + packetNumber, connectionId.length(), peerConnId.length(), + SMALLEST_MAXIMUM_DATAGRAM_SIZE); + maxPayloadSize = maxPayloadSize - ackSize; + + final CryptoFrame crypto = flow.localHandshake.produceFrame(maxPayloadSize); + assert crypto != null : "Handshake data was available (" + + flow.localHandshake.remaining() + " bytes) for sending, but no CRYPTO" + + " frame was produced, for max frame size: " + maxPayloadSize; + int cryptoSize = crypto.size(); + assert cryptoSize <= maxPayloadSize : cryptoSize - maxPayloadSize; + List frames = makeList(crypto, ackFrame); + + if (debug.on()) { + debug.log("building handshake packet: source=%s, dest=%s", + connectionId, peerConnId); + } + OutgoingQuicPacket packet = encoder.newHandshakePacket( + connectionId, peerConnId, + packetNumber, largestAckedPN, frames, codingContext); + int size = packet.size(); + if (debug.on()) { + debug.log("handshake packet size is %d, max is %d", + size, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + } + assert size <= SMALLEST_MAXIMUM_DATAGRAM_SIZE : size - SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + if (debug.on()) { + debug.log("protecting handshake quic hello packet for %s(%s) - %d bytes", + Arrays.toString(quicTLSEngine.getSSLParameters().getApplicationProtocols()), + peerAddress(), packet.size()); + } + pushDatagram(ProtectionRecord.single(packet, this::allocateDatagramForEncryption)); + var handshakeState = quicTLSEngine.getHandshakeState(); + if (debug.on()) { + debug.log("Handshake state is now: %s", handshakeState); + } + if (flow.localHandshake.remaining() == 0 + && quicTLSEngine.isTLSHandshakeComplete() + && !flow.handshakeCF.isDone()) { + if (stateHandle.markHandshakeComplete()) { + if (debug.on()) { + debug.log("Handshake completed"); + } + completeHandshakeCF(); + } + } + if (!packetSpaces.initial().isClosed() && flow.localInitial.remaining() > 0) { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: local initial has remaining, starting INITIAL transmitter", logTag())); + } + packetSpaces.initial.runTransmitter(); + } + } else { + return false; + } + return true; + } + + public QuicTransportParameters peerTransportParameters() { + return peerTransportParameters; + } + + public QuicTransportParameters localTransportParameters() { + return localTransportParameters; + } + + protected void consumeQuicParameters(final ByteBuffer byteBuffer) throws QuicTransportException { + final QuicTransportParameters params = QuicTransportParameters.decode(byteBuffer); + if (debug.on()) { + debug.log("Received peer Quic transport params: %s", params.toStringWithValues()); + } + final QuicConnectionId retryConnId = this.peerConnIdManager.retryConnId(); + if (params.isPresent(ParameterId.retry_source_connection_id)) { + if (retryConnId == null) { + throw new QuicTransportException("Retry connection ID was set even though no retry was performed", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } else if (!params.matches(ParameterId.retry_source_connection_id, retryConnId)) { + throw new QuicTransportException("Retry connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } else { + if (retryConnId != null) { + throw new QuicTransportException("Retry connection ID was expected but absent", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + if (!params.isPresent(ParameterId.original_destination_connection_id)) { + throw new QuicTransportException( + "Original connection ID transport parameter missing", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + + } + if (!params.isPresent(initial_source_connection_id)) { + throw new QuicTransportException( + "Initial source connection ID transport parameter missing", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + + } + final QuicConnectionId clientSelectedPeerConnId = this.peerConnIdManager.originalServerConnId(); + if (!params.matches(ParameterId.original_destination_connection_id, clientSelectedPeerConnId)) { + throw new QuicTransportException( + "Original connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (!params.matches(initial_source_connection_id, incomingInitialPacketSourceId)) { + throw new QuicTransportException( + "Initial source connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + // RFC-9000, section 18.2: A server that chooses a zero-length connection ID MUST NOT + // provide a preferred address. + if (peerConnectionId().length() == 0 && + params.isPresent(ParameterId.preferred_address)) { + throw new QuicTransportException( + "Preferred address present but connection ID has zero length", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(active_connection_id_limit)) { + final long limit = params.getIntParameter(active_connection_id_limit); + if (limit < 2) { + throw new QuicTransportException( + "Invalid active_connection_id_limit " + limit, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + if (params.isPresent(ParameterId.stateless_reset_token)) { + final byte[] statelessResetToken = params.getParameter(ParameterId.stateless_reset_token); + if (statelessResetToken.length != RESET_TOKEN_LENGTH) { + // RFC states 16 bytes for stateless token + throw new QuicTransportException( + "Invalid stateless reset token length " + statelessResetToken.length, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + VersionInformation vi = + params.getVersionInformationParameter(version_information); + if (vi != null) { + if (vi.chosenVersion() != quicVersion().versionNumber()) { + throw new QuicTransportException( + "[version_information] Chosen Version does not match version in use", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + if (processedVersionsPacket) { + if (vi.availableVersions().length == 0) { + throw new QuicTransportException( + "[version_information] available versions empty", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + if (Arrays.stream(vi.availableVersions()) + .anyMatch(i -> i == originalVersion.versionNumber())) { + throw new QuicTransportException( + "[version_information] original version was available", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + } else { + if (processedVersionsPacket && quicVersion != QuicVersion.QUIC_V1) { + throw new QuicTransportException( + "version_information parameter absent", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + handleIncomingPeerTransportParams(params); + + // params.setIntParameter(ParameterId.max_idle_timeout, TimeUnit.SECONDS.toMillis(30)); + // params.setParameter(ParameterId.stateless_reset_token, ...); // no token + // params.setIntParameter(ParameterId.initial_max_data, DEFAULT_INITIAL_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_bidi_local, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_bidi_remote, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_uni, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_streams_bidi, DEFAULT_MAX_STREAMS); + // params.setIntParameter(ParameterId.initial_max_streams_uni, DEFAULT_MAX_STREAMS); + // params.setIntParameter(ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(ParameterId.disable_active_migration, false); + // params.setPreferredAddressParameter(ParameterId.preferred_address, ...); + // params.setIntParameter(ParameterId.active_connection_id_limit, 2); + } + + /** + * {@return the number of (active) connection ids that this endpoint is willing + * to accept from the peer for a given connection} + */ + protected long getLocalActiveConnIDLimit() { + // currently we don't accept anything more than 2 (the RFC defined default minimum) + return 2; + } + + /** + * {@return the number of (active) connection ids that the peer is willing to accept + * for a given connection} + */ + protected long getPeerActiveConnIDLimit() { + return this.peerActiveConnIdsLimit; + } + + protected ByteBuffer buildInitialParameters() { + final QuicTransportParameters params = new QuicTransportParameters(this.transportParams); + setIntParamIfNotSet(params, active_connection_id_limit, this::getLocalActiveConnIDLimit); + final long idleTimeoutMillis = TimeUnit.SECONDS.toMillis( + Utils.getLongProperty("jdk.httpclient.quic.idleTimeout", 30)); + setIntParamIfNotSet(params, max_idle_timeout, () -> idleTimeoutMillis); + setIntParamIfNotSet(params, max_udp_payload_size, () -> { + assert this.endpoint != null : "Endpoint hasn't been set"; + return (long) this.endpoint.getMaxUdpPayloadSize(); + }); + setIntParamIfNotSet(params, initial_max_data, () -> DEFAULT_INITIAL_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_local, + () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_uni, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_remote, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_streams_uni, () -> (long) DEFAULT_MAX_UNI_STREAMS); + setIntParamIfNotSet(params, initial_max_streams_bidi, () -> (long) DEFAULT_MAX_BIDI_STREAMS); + if (!params.isPresent(initial_source_connection_id)) { + params.setParameter(initial_source_connection_id, connectionId.getBytes()); + } + if (!params.isPresent(version_information)) { + final VersionInformation vi = + QuicTransportParameters.buildVersionInformation(quicVersion, + quicInstance().getAvailableVersions()); + params.setVersionInformationParameter(version_information, vi); + } + // params.setIntParameter(ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(ParameterId.disable_active_migration, false); + final ByteBuffer buf = ByteBuffer.allocate(params.size()); + params.encode(buf); + buf.flip(); + if (debug.on()) { + debug.log("local transport params: %s", params.toStringWithValues()); + } + newLocalTransportParameters(params); + return buf; + } + + protected static void setIntParamIfNotSet(final QuicTransportParameters params, + final ParameterId paramId, + final Supplier valueSupplier) { + if (params.isPresent(paramId)) { + return; + } + params.setIntParameter(paramId, valueSupplier.get()); + } + + // the token to be included in initial packets, if any. + private byte[] initialToken() { + return initialToken; + } + + protected void newLocalTransportParameters(QuicTransportParameters params) { + localTransportParameters = params; + oneRttRcvQueue.newLocalParameters(params); + streams.newLocalTransportParameters(params); + final long idleTimeout = params.getIntParameter(max_idle_timeout, 0); + this.idleTimeoutManager.localIdleTimeout(idleTimeout); + } + + private List ackOrPing(AckFrame ack, boolean sendPing) { + if (sendPing) { + return ack == null ? List.of(new PingFrame()) : List.of(new PingFrame(), ack); + } + assert ack != null; + return List.of(ack); + } + + /** + * Emit a possibly non ACK-eliciting packet containing the given ACK frame. + * @param packetSpaceManager the packet space manager on behalf + * of which the acknowledgement should + * be sent. + * @param ackFrame the ACK frame to be sent. + * @param sendPing whether a PING frame should be sent. + * @return the emitted packet number, or -1L if not applicable or not emitted + */ + private long emitAckPacket(final PacketSpace packetSpaceManager, final AckFrame ackFrame, + final boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + if (ackFrame == null && !sendPing) { + return -1L; + } + if (debug.on()) { + if (sendPing) { + debug.log("Sending PING packet %s ack", + ackFrame == null ? "without" : "with"); + } else { + debug.log("sending ACK packet"); + } + } + final List frames = ackOrPing(ackFrame, sendPing); + final PacketNumberSpace packetNumberSpace = packetSpaceManager.packetNumberSpace(); + if (debug.on()) { + debug.log("Sending packet for %s, frame=%s", packetNumberSpace, frames); + } + final KeySpace keySpace = switch (packetNumberSpace) { + case APPLICATION -> KeySpace.ONE_RTT; + case HANDSHAKE -> KeySpace.HANDSHAKE; + case INITIAL -> KeySpace.INITIAL; + default -> throw new UnsupportedOperationException( + "Invalid packet number space: " + packetNumberSpace); + }; + if (sendPing && Log.quicRetransmit()) { + Log.logQuic("{0} {1}: sending PingFrame", logTag(), keySpace); + } + final QuicPacket ackpacket = encoder.newOutgoingPacket(keySpace, + packetSpaceManager, localConnectionId(), + peerConnectionId(), initialToken(), frames, codingContext); + pushDatagram(ProtectionRecord.single(ackpacket, this::allocateDatagramForEncryption)); + return ackpacket.packetNumber(); + } + + private LinkedList removeOutdatedFrames(List frames) { + // Remove frames that should not be retransmitted + LinkedList result = new LinkedList<>(); + for (QuicFrame f : frames) { + if (!(f instanceof PaddingFrame) && + !(f instanceof AckFrame) && + !(f instanceof PathChallengeFrame) && + !(f instanceof PathResponseFrame)) { + result.add(f); + } + } + return result; + } + + /** + * Retransmit the given packet on behalf of the given packet space + * manager. + * @param packetSpaceManager the packet space manager on behalf of + * which the packet is being retransmitted + * @param packet the unacknowledged packet which should be retransmitted + */ + private void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) { + debug.log("Retransmitting packet [type=%s, pn=%d, attempts:%d]: %s", + packet.packetType(), packet.packetNumber(), attempts, packet); + } + + assert packetSpaceManager.packetNumberSpace() == packet.numberSpace(); + long oldPacketNumber = packet.packetNumber(); + assert oldPacketNumber >= 0; + + long largestAckedPN = packetSpaceManager.getLargestPeerAckedPN(); + long newPacketNumber = packetSpaceManager.allocateNextPN(); + final int maxDatagramSize = getMaxDatagramSize(); + final QuicConnectionId peerConnectionId = peerConnectionId(); + final int dstIdLength = peerConnectionId.length(); + final PacketNumberSpace packetNumberSpace = packetSpaceManager.packetNumberSpace(); + final int initialDstIdLength = MAX_CONNECTION_ID_LENGTH; // reserve space for the ID to grow + + int maxPayloadSize = switch (packetNumberSpace) { + case APPLICATION -> QuicPacketEncoder.computeMaxOneRTTPayloadSize( + codingContext, newPacketNumber, dstIdLength, maxDatagramSize, largestAckedPN); + case INITIAL -> QuicPacketEncoder.computeMaxInitialPayloadSize( + codingContext, computePacketNumberLength(newPacketNumber, + codingContext.largestAckedPN(PacketNumberSpace.INITIAL)), + ((InitialPacket) packet).tokenLength(), + localConnectionId().length(), initialDstIdLength, maxDatagramSize); + case HANDSHAKE -> QuicPacketEncoder.computeMaxHandshakePayloadSize( + codingContext, newPacketNumber, localConnectionId().length(), + dstIdLength, maxDatagramSize); + default -> throw new IllegalArgumentException( + "Invalid packet number space: " + packetNumberSpace); + }; + + // The new packet may have larger size(), which might no longer fit inside + // the maximum datagram size supported on the path. To avoid that, we + // strip the padding and old ack frame from the original packet, and + // include the new ack frame only if it fits in the available size. + LinkedList frames = removeOutdatedFrames(packet.frames()); + int size = frames.stream().mapToInt(QuicFrame::size).sum(); + int remaining = maxPayloadSize - size; + AckFrame ack = packetSpaceManager.getNextAckFrame(false, remaining); + if (ack != null) { + assert ack.size() <= remaining : "AckFrame size %s is bigger than %s" + .formatted(ack.size(), remaining); + frames.addFirst(ack); + } + QuicPacket retransmitted = + switch (packet.packetType()) { + case INITIAL -> encoder.newInitialPacket(localConnectionId(), + peerConnectionId, ((InitialPacket) packet).token(), + newPacketNumber, largestAckedPN, frames, + codingContext); + case HANDSHAKE -> encoder.newHandshakePacket(localConnectionId(), + peerConnectionId, newPacketNumber, largestAckedPN, + frames, codingContext); + case ONERTT, ZERORTT -> encoder.newOneRttPacket( + peerConnectionId, newPacketNumber, largestAckedPN, + frames, codingContext); + default -> throw new IllegalArgumentException("packetType: %s, packet: %s" + .formatted(packet.packetType(), packet.packetNumber())); + }; + + if (Log.quicRetransmit()) { + Log.logQuic("%s OUT: retransmitting packet [%s] pn:%s as pn:%s".formatted( + logTag(), packet.packetType(), oldPacketNumber, newPacketNumber)); + } + pushDatagram(ProtectionRecord.retransmitting(retransmitted, + oldPacketNumber, + this::allocateDatagramForEncryption)); + } + + @Override + public CompletableFuture requestSendPing() { + final KeySpace space = quicTLSEngine.getCurrentSendKeySpace(); + final PacketSpace spaceManager = packetSpaces.get(PacketNumberSpace.of(space)); + return spaceManager.requestSendPing(); + } + + /** + * {@return the underlying {@code NetworkChannel} used by this connection, + * which may be {@code null} if the endpoint has not been configured yet} + */ + public NetworkChannel channel() { + QuicEndpoint endpoint = this.endpoint; + return endpoint == null ? null : endpoint.channel(); + } + + @Override + public String toString() { + return cachedToString; + } + + @Override + public boolean isOpen() { + return stateHandle.opened(); + } + + @Override + public TerminationCause terminationCause() { + return terminator.getTerminationCause(); + } + + public final CompletableFuture futureTerminationCause() { + return terminator.futureTerminationCause(); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return this.terminator; + } + + /** + * {@return true if this connection is a client connection} + * Server side connections will return false. + */ + public boolean isClientConnection() { + return true; + } + + /** + * Called when new quic transport parameters are available from the peer. + * @param params the peer's new quic transport parameter + */ + protected void handleIncomingPeerTransportParams(final QuicTransportParameters params) { + peerTransportParameters = params; + this.idleTimeoutManager.peerIdleTimeout(params.getIntParameter(max_idle_timeout)); + // when we reach here, the value for max_udp_payload_size has already been + // asserted that it isn't outside the allowed range of 1200 to 65527. That has + // happened in QuicTransportParameters.checkParameter(). + // intentional cast to int since the value will be within int range + maxPeerAdvertisedPayloadSize = (int) params.getIntParameter(max_udp_payload_size); + congestionController.updateMaxDatagramSize(getMaxDatagramSize()); + if (params.isPresent(ParameterId.initial_max_data)) { + oneRttSndQueue.setMaxData(params.getIntParameter(ParameterId.initial_max_data), true); + } + streams.newPeerTransportParameters(params); + packetSpaces.app().updatePeerTransportParameters( + params.getIntParameter(ParameterId.max_ack_delay), + params.getIntParameter(ParameterId.ack_delay_exponent)); + // param value for this param is already validated outside of this method, so we just + // set the value without any validations + this.peerActiveConnIdsLimit = params.getIntParameter(active_connection_id_limit); + if (params.isPresent(ParameterId.stateless_reset_token)) { + // the stateless reset token for the handshake connection id + final byte[] statelessResetToken = params.getParameter(ParameterId.stateless_reset_token); + // register with peer connid manager + this.peerConnIdManager.handshakeStatelessResetToken(statelessResetToken); + } + if (params.isPresent(ParameterId.preferred_address)) { + final byte[] val = params.getParameter(ParameterId.preferred_address); + final ByteBuffer preferredConnId = QuicTransportParameters.getPreferredConnectionId(val); + final byte[] preferredStatelessResetToken = QuicTransportParameters + .getPreferredStatelessResetToken(val); + this.peerConnIdManager.handlePreferredAddress(preferredConnId, preferredStatelessResetToken); + } + if (debug.on()) { + debug.log("incoming peer parameters handled"); + } + } + + protected void incomingInitialFrame(final AckFrame frame) throws QuicTransportException { + packetSpaces.initial.processAckFrame(frame); + if (!handshakeFlow.handshakeReachedPeerCF.isDone()) { + if (debug.on()) debug.log("completing handshakeStartedCF normally"); + handshakeFlow.handshakeReachedPeerCF.complete(null); + } + } + + protected int incomingInitialFrame(final CryptoFrame frame) throws QuicTransportException { + // make sure to provide the frames in order, and + // buffer them if at the wrong offset + if (!handshakeFlow.handshakeReachedPeerCF.isDone()) { + if (debug.on()) debug.log("completing handshakeStartedCF normally"); + handshakeFlow.handshakeReachedPeerCF.complete(null); + } + final CryptoDataFlow peerInitial = handshakeFlow.peerInitial; + final long buffer = frame.offset() + frame.length() - peerInitial.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.INITIAL, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + int provided = 0; + CryptoFrame nextFrame = peerInitial.receive(frame); + while (nextFrame != null) { + if (debug.on()) { + debug.log("Provide crypto frame to engine: %s", nextFrame); + } + quicTLSEngine.consumeHandshakeBytes(KeySpace.INITIAL, nextFrame.payload()); + provided += nextFrame.length(); + nextFrame = peerInitial.poll(); + if (debug.on()) { + debug.log("Provided: " + provided); + } + } + return provided; + } + + protected void incomingInitialFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingInitialFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingInitialFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incomingHandshakeFrame(final AckFrame frame) throws QuicTransportException { + packetSpaces.handshake.processAckFrame(frame); + } + + protected int incomingHandshakeFrame(final CryptoFrame frame) throws QuicTransportException { + final CryptoDataFlow peerHandshake = handshakeFlow.peerHandshake; + // make sure to provide the frames in order, and + // buffer them if at the wrong offset + final long buffer = frame.offset() + frame.length() - peerHandshake.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.HANDSHAKE, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + int provided = 0; + CryptoFrame nextFrame = peerHandshake.receive(frame); + while (nextFrame != null) { + quicTLSEngine.consumeHandshakeBytes(KeySpace.HANDSHAKE, nextFrame.payload()); + provided += nextFrame.length(); + nextFrame = peerHandshake.poll(); + } + return provided; + } + + protected void incomingHandshakeFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingHandshakeFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingHandshakeFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incoming1RTTFrame(final AckFrame ackFrame) throws QuicTransportException { + packetSpaces.app.processAckFrame(ackFrame); + } + + protected void incoming1RTTFrame(final StreamFrame frame) throws QuicTransportException { + final long streamId = frame.streamId(); + final QuicReceiverStream stream = getReceivingStream(streamId, frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final CryptoFrame frame) throws QuicTransportException { + final long buffer = frame.offset() + frame.length() - peerCryptoFlow.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.ONE_RTT, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + CryptoFrame nextFrame = peerCryptoFlow.receive(frame); + while (nextFrame != null) { + quicTLSEngine.consumeHandshakeBytes(KeySpace.ONE_RTT, nextFrame.payload()); + nextFrame = peerCryptoFlow.poll(); + } + } + + protected void incoming1RTTFrame(final ResetStreamFrame frame) throws QuicTransportException { + final long streamId = frame.streamId(); + final QuicReceiverStream stream = getReceivingStream(streamId, frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final StreamDataBlockedFrame frame) + throws QuicTransportException { + final QuicReceiverStream stream = getReceivingStream(frame.streamId(), frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final DataBlockedFrame frame) throws QuicTransportException { + // TODO implement similar logic as STREAM_DATA_BLOCKED frame receipt + // and increment gradually if consumption is more than 1/4th the window size of the + // connection + } + + protected void incoming1RTTFrame(final StreamsBlockedFrame frame) + throws QuicTransportException { + if (frame.maxStreams() > MAX_STREAMS_VALUE_LIMIT) { + throw new QuicTransportException("Invalid maxStreams value %s" + .formatted(frame.maxStreams()), + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + streams.peerStreamsBlocked(frame); + } + + protected void incoming1RTTFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incoming1RTTFrame(final MaxDataFrame frame) throws QuicTransportException { + oneRttSndQueue.setMaxData(frame.maxData(), false); + } + + protected void incoming1RTTFrame(final MaxStreamDataFrame frame) + throws QuicTransportException { + final long streamId = frame.streamID(); + final QuicSenderStream stream = getSendingStream(streamId, frame.getTypeField()); + if (stream != null) { + streams.setMaxStreamData(stream, frame.maxStreamData()); + } + } + + protected void incoming1RTTFrame(final MaxStreamsFrame frame) throws QuicTransportException { + if (frame.maxStreams() >> 60 != 0) { + throw new QuicTransportException("Invalid maxStreams value %s" + .formatted(frame.maxStreams()), + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final boolean increased = streams.tryIncreaseStreamLimit(frame); + if (debug.on()) { + debug.log((increased ? "increased" : "did not increase") + + " " + (frame.isBidi() ? "bidi" : "uni") + + " stream limit to " + frame.maxStreams()); + } + } + + protected void incoming1RTTFrame(final StopSendingFrame frame) throws QuicTransportException { + final long streamId = frame.streamID(); + final QuicSenderStream stream = getSendingStream(streamId, frame.getTypeField()); + if (stream != null) { + streams.stopSendingReceived(stream, + frame.errorCode()); + } + } + + protected void incoming1RTTFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incoming1RTTFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incoming1RTTFrame(final HandshakeDoneFrame frame) + throws QuicTransportException { + if (quicTLSEngine.tryReceiveHandshakeDone()) { + // now that HANDSHAKE_DONE is received (and thus handshake confirmed), + // close the HANDSHAKE packet space (and thus discard the keys) + if (debug.on()) { + debug.log("received HANDSHAKE_DONE from server, initiating close of" + + " HANDSHAKE packet space"); + } + packetSpaces.handshake.close(); + } + packetSpaces.app.confirmHandshake(); + } + + protected void incoming1RTTFrame(final NewConnectionIDFrame frame) + throws QuicTransportException { + if (peerConnectionId().length() == 0) { + throw new QuicTransportException( + "NEW_CONNECTION_ID not allowed here", + null, frame.getTypeField(), PROTOCOL_VIOLATION); + } + this.peerConnIdManager.handleNewConnectionIdFrame(frame); + } + + protected void incoming1RTTFrame(final OneRttPacket oneRttPacket, + final RetireConnectionIDFrame frame) + throws QuicTransportException { + this.localConnIdManager.handleRetireConnectionIdFrame(oneRttPacket.destinationId(), + PacketType.ONERTT, frame); + } + + protected void incoming1RTTFrame(final NewTokenFrame frame) throws QuicTransportException { + // as per RFC 9000, section 19.7, token cannot be empty and if it is, then + // a connection error of type FRAME_ENCODING_ERROR needs to be raised + final byte[] newToken = frame.token(); + if (newToken.length == 0) { + throw new QuicTransportException("Empty token in NEW_TOKEN frame", + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + assert this.quicInstance instanceof QuicClient : "Not a QuicClient"; + final QuicClient quicClient = (QuicClient) this.quicInstance; + // set this as the initial token to be used in INITIAL packets when attempting + // any new subsequent connections against this same target server + quicClient.registerInitialToken(this.peerAddress, newToken); + if (debug.on()) { + debug.log("Registered a new (initial) token for peer " + this.peerAddress); + } + } + + protected void incoming1RTTFrame(final PathResponseFrame frame) + throws QuicTransportException { + throw new QuicTransportException("Unmatched PATH_RESPONSE frame", + KeySpace.ONE_RTT, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + protected void incoming1RTTFrame(final PathChallengeFrame frame) + throws QuicTransportException { + pathChallengeFrameQueue.offer(frame); + if (pathChallengeFrameQueue.size() > 3) { + // we don't expect to hold more than 1 PathChallenge per path. + // If there's more than 3 outstanding challenges, drop the oldest one. + pathChallengeFrameQueue.poll(); + } + } + + /** + * Signal the connection that some stream data is available for sending on one or more streams. + * @param streamIds the stream ids + */ + public void streamDataAvailableForSending(final Set streamIds) { + for (final long id : streamIds) { + streams.enqueueForSending(id); + } + packetSpaces.app.runTransmitter(); + } + + /** + * Called when the receiving part or the sending part of a stream + * reaches a terminal state. + * @param streamId the id of the stream + * @param state the terminal state + */ + public void notifyTerminalState(long streamId, StreamState state) { + assert state.isTerminal() : state; + streams.notifyTerminalState(streamId, state); + } + + /** + * Called to request sending of a RESET_STREAM frame. + * + * @apiNote + * Should only be called for sending streams. For stopping a + * receiving stream then {@link #scheduleStopSendingFrame(long, long)} should be called. + * This method should only be called from {@code QuicSenderStreamImpl}, after + * switching the state of the stream to RESET_SENT. + * + * @param streamId the id of the stream that should be reset + * @param errorCode the application error code + */ + public void requestResetStream(long streamId, long errorCode) { + assert streams.isSendingStream(streamId); + streams.requestResetStream(streamId, errorCode); + packetSpaces.app.runTransmitter(); + } + + /** + * Called to request sending of a STOP_SENDING frame. + * @apiNote + * Should only be called for receiving streams. For stopping a + * sending stream then {@link #requestResetStream(long, long)} + * should be called. + * This method should only be called from {@code QuicReceiverStreamImpl} + * @param streamId the stream id to be cancelled + * @param errorCode the application error code + */ + public void scheduleStopSendingFrame(long streamId, long errorCode) { + assert streams.isReceivingStream(streamId); + streams.scheduleStopSendingFrame(new StopSendingFrame(streamId, errorCode)); + packetSpaces.app.runTransmitter(); + } + + /** + * Called to request sending of a MAX_STREAM_DATA frame. + * @apiNote + * Should only be called for receiving streams. + * This method should only be called from {@code QuicReceiverStreamImpl} + * @param streamId the stream id to be cancelled + * @param maxStreamData the new max data we are prepared to receive on + * this stream + */ + public void requestSendMaxStreamData(long streamId, long maxStreamData) { + assert streams.isReceivingStream(streamId); + streams.requestSendMaxStreamData(new MaxStreamDataFrame(streamId, maxStreamData)); + packetSpaces.app.runTransmitter(); + } + + /** + * Called when frame data can be safely added to the amount of + * data received by the connection for MAX_DATA flow control + * purpose. + * @throws QuicTransportException if flow control was exceeded + * @param frameType type of frame received + */ + public void increaseReceivedData(long diff, long frameType) throws QuicTransportException { + oneRttRcvQueue.checkAndIncreaseReceivedData(diff, frameType); + } + + /** + * Called when frame data is removed from the connection + * and the amount of data can be added to MAX_DATA window. + * @param diff amount of data processed + */ + public void increaseProcessedData(long diff) { + oneRttRcvQueue.increaseProcessedData(diff); + } + + public QuicTLSEngine getTLSEngine() { + return quicTLSEngine; + } + + /** + * {@return the computed PTO for the current packet number space, + * adjusted by our max ack delay} + */ + public long peerPtoMs() { + return rttEstimator.getBasePtoDuration().toMillis() + + (quicTLSEngine.getCurrentSendKeySpace() == KeySpace.ONE_RTT ? + PacketSpaceManager.ADVERTISED_MAX_ACK_DELAY : 0); + } + + public void runAppPacketSpaceTransmitter() { + this.packetSpaces.app.runTransmitter(); + } + + public void shutdown() { + packetSpaces.close(); + } + + public final String logTag() { + return logTag; + } + + /* ======================================================== + * Direct Byte Buffer Pool + * ======================================================== */ + + // Maximum size of the connection's Direct ByteBuffer Pool. + // For a connection configured to attempt sending datagrams in thread + // (QuicEndpoint.SEND_DGRAM_ASYNC == false), 2 should be enough, as we + // shouldn't have more than 2 packet number spaces active at the same time. + private static final int MAX_DBB_POOL_SIZE = 3; + // The ByteBuffer pool, which contains available byte buffers + private final ConcurrentLinkedQueue bbPool = new ConcurrentLinkedQueue<>(); + // The number of Direct Byte Buffers allocated for sending and managed by the pool. + // This is the number of Direct Byte Buffers currently in flight, plus the number + // of available byte buffers present in the pool. It will never exceed + // MAX_DBB_POOL_SIZE. + private final AtomicInteger bbAllocated = new AtomicInteger(); + + // Some counters used for printing debug statistics when Log quic:dbb is enabled + // Byte Buffers in flight: the number of byte buffers that were returned by + // getOutgoingByteBuffer() minus the number of byte buffers that were released + // through datagramReleased() + private final AtomicInteger bbInFlight = new AtomicInteger(); + // Peak number of byte buffers in flight. Never decreases. + private final AtomicInteger bbPeak = new AtomicInteger(); + // Number of unreleased byte buffers. This should eventually reach 0. + final AtomicInteger bbUnreleased = new AtomicInteger(); + + /** + * {@return a new {@code ByteBuffer} to encode and encrypt packets in a datagram} + * This method may either allocate a new heap BteBuffer or return a (possibly + * new) Direct ByteBuffer from the connection's Direct Byte Buffer Pool. + * @param size the maximum size of the datagram + */ + protected ByteBuffer getOutgoingByteBuffer(int size) { + bbUnreleased.incrementAndGet(); + if (USE_DIRECT_BUFFER_POOL) { + if (size <= getMaxDatagramSize()) { + ByteBuffer buffer = bbPool.poll(); + if (buffer != null) { + if (buffer.limit() >= getMaxDatagramSize()) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: got direct buffer from pool" + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return buffer; + } + bbAllocated.decrementAndGet(); + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: releasing direct buffer"); + } + buffer = null; + } + + assert buffer == null; + int allocated; + while ((allocated = bbAllocated.get()) < MAX_DBB_POOL_SIZE) { + if (bbAllocated.compareAndSet(allocated, allocated + 1)) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: allocating direct buffer #" + (allocated + 1) + + ", inFlight: " + bbInFlight.get() + ", peak: " + + bbPeak.get() + ", unreleased:" + bbUnreleased.get()); + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return ByteBuffer.allocateDirect(getMaxDatagramSize()); + } + } + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: too many buffers allocated: " + allocated + + ", inFlight: " + bbInFlight.get() + ", peak: " + + bbPeak.get() + ", unreleased:" + bbUnreleased.get()); + } + + } else { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: wrong size " + size); + } + } + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return ByteBuffer.allocate(size); + } + @Override + public void datagramSent(QuicDatagram datagram) { + datagramReleased(datagram); + } + + @Override + public void datagramDiscarded(QuicDatagram datagram) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram discarded " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + datagramReleased(datagram); + } + + public void datagramDropped(QuicDatagram datagram) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram dropped " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + datagramReleased(datagram); + } + + /** + * Returns a {@link jdk.internal.net.http.quic.QuicEndpoint.Datagram} which contains + * an encrypted QUIC packet containing + * a {@linkplain ConnectionCloseFrame CONNECTION_CLOSE frame}. The CONNECTION_CLOSE + * frame will have a frame type of {@code 0x1c} and error code of {@code NO_ERROR}. + *

    + * This method should only be invoked when the {@link QuicEndpoint} is being closed + * and the endpoint wants to send out a {@code CONNECTION_CLOSE} frame on a best-effort + * basis (in a fire and forget manner). + * + * @return the datagram containing the QUIC packet with a CONNECTION_CLOSE frame or + * an {@linkplain Optional#empty() empty Optional} if the datagram couldn't + * be constructed. + */ + final Optional connectionCloseDatagram() { + try { + final ByteBuffer quicPktPayload = this.terminator.makeConnectionCloseDatagram(); + return Optional.of(new QuicDatagram(this, peerAddress, quicPktPayload)); + } catch (Exception e) { + // ignore any exception because providing the connection close datagram + // when the endpoint is being closed, is on best-effort basis + return Optional.empty(); + } + } + + /** + * Called when a datagram is being released, either from + * {@link #datagramSent(QuicDatagram)}, {@link #datagramDiscarded(QuicDatagram)}, + * or {@link #datagramDropped(QuicDatagram)}. + * This method may either release the datagram and let it get garbage collected, + * or return it to the pool. + * @param datagram the released datagram + */ + protected void datagramReleased(QuicDatagram datagram) { + bbUnreleased.decrementAndGet(); + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram released " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + bbInFlight.decrementAndGet(); + if (USE_DIRECT_BUFFER_POOL) { + ByteBuffer buffer = datagram.payload(); + buffer.clear(); + if (buffer.isDirect()) { + if (buffer.limit() >= getMaxDatagramSize()) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: offering buffer to pool"); + } + bbPool.offer(buffer); + } else { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: releasing direct buffer (too small)"); + } + bbAllocated.decrementAndGet(); + } + } + } + } + + public String loggableState() { + // for HTTP3 debugging + // If the connection was active (open bidi streams), log connection state + if (streams.quicStreams().noneMatch(QuicStream::isBidirectional)) { + // no active requests + return "No active requests"; + } + Deadline now = TimeSource.now(); + StringBuilder result = new StringBuilder("sending: {canSend:" + oneRttSndQueue.canSend() + + ", credit: " + oneRttSndQueue.credit() + + ", sendersReady: " + streams.hasAvailableData() + + ", hasControlFrames: " + streams.hasControlFrames() + + "}, cc: { backoff: " + rttEstimator.getPtoBackoff() + + ", duration: " + ((PacketSpaceManager) packetSpaces.app).getPtoDuration() + + ", current deadline: " + Utils.debugDeadline(now, + ((PacketSpaceManager) packetSpaces.app).deadline()) + + ", prospective deadline: " + Utils.debugDeadline(now, + ((PacketSpaceManager) packetSpaces.app).prospectiveDeadline()) + + "}, streams: ["); + streams.quicStreams().filter(QuicStream::isBidirectional).forEach( + s -> { + QuicBidiStreamImpl qb = (QuicBidiStreamImpl) s; + result.append("{id:" + s.streamId() + + ", available: " + qb.senderPart().available() + + ", blocked: " + qb.senderPart().isBlocked() + "}," + ); + } + ); + result.append("]"); + return result.toString(); + } + + /** + * {@return true if the packet contains a CONNECTION_CLOSE frame, false otherwise} + * @param packet the QUIC packet + */ + private static boolean containsConnectionClose(final QuicPacket packet) { + for (final QuicFrame frame : packet.frames()) { + if (frame instanceof ConnectionCloseFrame) { + return true; + } + } + return false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java new file mode 100644 index 00000000000..3df9b599f82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java @@ -0,0 +1,2062 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicSelector.QuicNioSelector; +import jdk.internal.net.http.quic.QuicSelector.QuicVirtualThreadPoller; +import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.util.OperatingSystem; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; + +import static jdk.internal.net.http.quic.QuicEndpoint.ChannelType.BLOCKING_WITH_VIRTUAL_THREADS; +import static jdk.internal.net.http.quic.QuicEndpoint.ChannelType.NON_BLOCKING_WITH_SELECTOR; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; + + +/** + * A QUIC Endpoint. A QUIC endpoint encapsulate a DatagramChannel + * and is registered with a Selector. It subscribes for read and + * write events from the selector, and implements a readLoop and + * a writeLoop. + *

    + * The read event or write event are triggered by the selector + * thread. When the read event is triggered, all available datagrams + * are read from the channel and pushed into a read queue. + * Then the readLoop is triggered. + * When the write event is triggered, the key interestOps are + * modified to pause write events, and the writeLoop is triggered. + *

    + * The readLoop and writeLoop should never execute on the selector + * thread, but rather, in the client's executor. + *

    + * When the writeLoop is triggered, it polls the writeQueue and + * writes as many datagram as it can to the channel. At the end, + * if there still remains some datagrams in the writeQueue, the + * write event is resumed. Otherwise, the writeLoop is next + * triggered when new datagrams are added to the writeQueue. + *

    + * When the readLoop is triggered, it polls the read queue + * and attempts to match each received packet with a + * QuicConnection. If no connection matches, it attempts + * to match the packet with stateless reset tokens. + * If no stateless reset token match, the packet is + * discarded. + *

    + */ +public abstract sealed class QuicEndpoint implements AutoCloseable + permits QuicEndpoint.QuicSelectableEndpoint, QuicEndpoint.QuicVirtualThreadedEndpoint { + + private static final int INCOMING_MAX_DATAGRAM; + static final boolean DGRAM_SEND_ASYNC; + static final int MAX_BUFFERED_HIGH; + static final int MAX_BUFFERED_LOW; + static { + // This default value is the maximum payload size of + // an IPv6 datagram, which is 65527 (which is bigger + // than that of an IPv4). + // We have only one direct buffer of this size per endpoint. + final int defSize = 65527; + // This is the value that will be transmitted to the server in the + // max_udp_payload_size parameter + int size = Utils.getIntegerProperty("jdk.httpclient.quic.maxUdpPayloadSize", defSize); + // don't allow the value to be below 1200 and above 65527, to conform with RFC-9000, + // section 18.2. + if (size < 1200 || size > 65527) { + // fallback to default size + size = defSize; + } + INCOMING_MAX_DATAGRAM = size; + // TODO: evaluate pros and cons WRT performance and decide for one or the other + // before GA. + DGRAM_SEND_ASYNC = Utils.getBooleanProperty("jdk.internal.httpclient.quic.sendAsync", false); + int maxBufferHigh = Math.clamp(Utils.getIntegerProperty("jdk.httpclient.quic.maxBufferedHigh", + 512 << 10), 128 << 10, 6 << 20); + int maxBufferLow = Math.clamp(Utils.getIntegerProperty("jdk.httpclient.quic.maxBufferedLow", + 384 << 10), 64 << 10, 6 << 20); + if (maxBufferLow >= maxBufferHigh) maxBufferLow = maxBufferHigh >> 1; + MAX_BUFFERED_HIGH = maxBufferHigh; + MAX_BUFFERED_LOW = maxBufferLow; + } + + /** + * This interface represent a UDP Datagram. This could be + * either an incoming datagram or an outgoing datagram. + */ + public sealed interface Datagram + permits QuicDatagram, StatelessReset, SendStatelessReset, UnmatchedDatagram { + /** + * {@return the peer address} + * For incoming datagrams, this is the sender address. + * For outgoing datagrams, this is the destination address. + */ + SocketAddress address(); + + /** + * {@return the datagram payload} + */ + ByteBuffer payload(); + } + + /** + * An incoming UDP Datagram for which no connection was found. + * On the server side it may represent a new connection attempt. + * @param address the {@linkplain Datagram#address() sender address} + * @param payload {@inheritDoc} + */ + public record UnmatchedDatagram(SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * A stateless reset that should be sent in response + * to an incoming datagram targeted at a deleted connection. + * @param address the {@linkplain Datagram#address() destination address} + * @param payload the outgoing stateless reset + */ + public record SendStatelessReset(SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * An incoming datagram containing a stateless reset + * @param connection the connection to reset + * @param address the {@linkplain Datagram#address() sender address} + * @param payload the datagram payload + */ + public record StatelessReset(QuicPacketReceiver connection, SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * An outgoing datagram, or an incoming datagram for which + * a connection was identified. + * @param connection the sending or receiving connection + * @param address {@inheritDoc} + * @param payload {@inheritDoc} + */ + public record QuicDatagram(QuicPacketReceiver connection, SocketAddress address, ByteBuffer payload) + implements Datagram {} + + /** + * An enum identifying the type of channels used and supported by QuicEndpoint and + * QuicSelector + */ + public enum ChannelType { + NON_BLOCKING_WITH_SELECTOR, + BLOCKING_WITH_VIRTUAL_THREADS; + public boolean isBlocking() { + return this == BLOCKING_WITH_VIRTUAL_THREADS; + } + } + + // A temporary internal property to switch between two QuicSelector implementation: + // - if true, uses QuicNioSelector, an implementation based non-blocking and channels + // and an NIO Selector + // - if false, uses QuicVirtualThreadPoller, an implementation that use Virtual Threads + // to poll blocking channels + // On windows, we default to using non-blocking IO with a Selector in order + // to avoid a potential deadlock in WEPoll (see JDK-8334574). + private static final boolean USE_NIO_SELECTOR = + Utils.getBooleanProperty("jdk.internal.httpclient.quic.useNioSelector", + OperatingSystem.isWindows()); + /** + * The configured channel type + */ + public static final ChannelType CONFIGURED_CHANNEL_TYPE = USE_NIO_SELECTOR + ? NON_BLOCKING_WITH_SELECTOR + : BLOCKING_WITH_VIRTUAL_THREADS; + + final Logger debug = Utils.getDebugLogger(this::name); + private final QuicInstance quicInstance; + private final String name; + private final DatagramChannel channel; + private final ByteBuffer receiveBuffer; + final Executor executor; + final ConcurrentLinkedQueue readQueue = new ConcurrentLinkedQueue<>(); + final ConcurrentLinkedQueue writeQueue = new ConcurrentLinkedQueue<>(); + final QuicTimerQueue timerQueue; + private volatile boolean closed; + + // A synchronous scheduler to consume the readQueue list; + final SequentialScheduler readLoopScheduler = + SequentialScheduler.lockingScheduler(this::readLoop); + + // A synchronous scheduler to consume the writeQueue list; + final SequentialScheduler writeLoopScheduler = + SequentialScheduler.lockingScheduler(this::writeLoop); + + // A ConcurrentMap to store registered connections. + // The connection IDs might come from external sources. They implement Comparable + // to mitigate collision attacks. + // This map must not share the idFactory with other maps, + // see RFC 9000 section 21.11. Stateless Reset Oracle + private final ConcurrentMap connections = + new ConcurrentHashMap<>(); + + // a factory of local connection IDs. + private final QuicConnectionIdFactory idFactory; + + // Key used to encrypt tokens before storing in {@link #peerIssuedResetTokens} + private final Key tokenEncryptionKey; + + // keeps a link of the peer issued stateless reset token to the corresponding connection that + // will be closed if the specific stateless reset token is received + private final ConcurrentMap peerIssuedResetTokens = + new ConcurrentHashMap<>(); + + private static ByteBuffer allocateReceiveBuffer() { + return ByteBuffer.allocateDirect(INCOMING_MAX_DATAGRAM); + } + + private final AtomicInteger buffered = new AtomicInteger(); + volatile boolean readingStalled; + + public QuicConnectionIdFactory idFactory() { + return idFactory; + } + + public int buffer(int bytes) { + return buffered.addAndGet(bytes); + } + + public int unbuffer(int bytes) { + var newval = buffered.addAndGet(-bytes); + assert newval >= 0; + if (newval <= MAX_BUFFERED_LOW) { + resumeReading(); + } + return newval; + } + + boolean bufferTooBig() { + return buffered.get() >= MAX_BUFFERED_HIGH; + } + + public int buffered() { + return buffered.get(); + } + + boolean readingPaused() { + return readingStalled; + } + + abstract void resumeReading(); + + abstract void pauseReading(); + + private QuicEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + this.quicInstance = quicInstance; + this.name = name; + this.channel = channel; + this.receiveBuffer = allocateReceiveBuffer(); + this.executor = quicInstance.executor(); + this.timerQueue = timerQueue; + if (debug.on()) debug.log("created for %s", channel); + try { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + tokenEncryptionKey = kg.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("AES key generator not available", e); + } + idFactory = isServer() + ? QuicConnectionIdFactory.getServer() + : QuicConnectionIdFactory.getClient(); + } + + public String name() { + return name; + } + + public DatagramChannel channel() { + return channel; + } + + Executor writeLoopExecutor() { return executor; } + + public SocketAddress getLocalAddress() throws IOException { + return channel.getLocalAddress(); + } + + public String getLocalAddressString() { + try { + return String.valueOf(channel.getLocalAddress()); + } catch (IOException io) { + return "No address available"; + } + } + + int getMaxUdpPayloadSize() { + return INCOMING_MAX_DATAGRAM; + } + + protected abstract ChannelType channelType(); + + /** + * A {@link QuicEndpoint} implementation based on non blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * NIO {@link Selector}. + * This implementation is tied to a {@link QuicNioSelector}. + */ + static final class QuicSelectableEndpoint extends QuicEndpoint { + volatile SelectionKey key; + + private QuicSelectableEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + super(quicInstance, channel, name, timerQueue); + assert !channel.isBlocking() : "SelectableQuicEndpoint channel is blocking"; + } + + @Override + public ChannelType channelType() { + return NON_BLOCKING_WITH_SELECTOR; + } + + /** + * Attaches this endpoint to a selector. + * + * @param selector the selector to attach to + * @throws ClosedChannelException if the channel is already closed + */ + public void attach(Selector selector) throws ClosedChannelException { + var key = this.key; + assert key == null; + // this block is needed to coordinate with detach() and + // selected(). See comment in selected(). + synchronized (this) { + this.key = super.channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); + } + } + + @Override + void resumeReading() { + boolean resumed = false; + SelectionKey key; + synchronized (this) { + key = this.key; + if (key != null && key.isValid()) { + if (isClosed() || isChannelClosed()) return; + int ops = key.interestOps(); + int newops = ops | SelectionKey.OP_READ; + if (ops != newops) { + key.interestOpsOr(SelectionKey.OP_READ); + readingStalled = false; + resumed = true; + } + } + } + if (resumed) { + // System.out.println(this + " endpoint resumed reading"); + if (debug.on()) debug.log("endpoint resumed reading"); + key.selector().wakeup(); + } + } + + @Override + void pauseReading() { + boolean paused = false; + synchronized (this) { + if (readingStalled) return; + if (key != null && key.isValid() && bufferTooBig()) { + if (isClosed() || isChannelClosed()) return; + int ops = key.interestOps(); + int newops = ops & ~SelectionKey.OP_READ; + if (ops != newops) { + key.interestOpsAnd(~SelectionKey.OP_READ); + readingStalled = true; + paused = true; + } + } + } + if (paused) { + // System.out.println(this + " endpoint paused reading"); + if (debug.on()) debug.log("endpoint paused reading"); + } + } + + /** + * Invoked by the {@link QuicSelector} when this endpoint's channel + * is selected. + * + * @param readyOps The operations that are ready for this endpoint. + */ + public void selected(int readyOps) { + var key = this.key; + try { + if (key == null) { + // null keys have been observed here. + // key can only be null if it's been cancelled, by detach() + // or if the call to channel::register hasn't returned yet + // the synchronized block below will block until + // channel::register returns if needed. + // This can only happen once, when attaching the channel, + // so there should be no performance issue in synchronizing + // here. + synchronized (this) { + key = this.key; + } + } + + if (key == null) { + if (debug.on()) { + debug.log("key is null"); + if (QuicEndpoint.class.desiredAssertionStatus()) { + Thread.dumpStack(); + } + } + return; + } + + if (debug.on()) { + debug.log("selected(interest=%s, ready=%s)", + Utils.interestOps(key), + Utils.readyOps(key)); + } + + int interestOps = key.interestOps(); + + // Some operations may be ready even when we are not interested. + // Typically, a channel may be ready for writing even if we have + // nothing to write. The events we need to invoke are therefore + // at the intersection of the ready set with the interest set. + int event = readyOps & interestOps; + if ((event & SelectionKey.OP_READ) == SelectionKey.OP_READ) { + onReadEvent(); + if (isClosed()) { + key.interestOpsAnd(~SelectionKey.OP_READ); + } + } + if ((event & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { + onWriteEvent(); + } + if (debug.on()) { + debug.log("interestOps: %s", Utils.interestOps(key)); + } + } finally { + if (!channel().isOpen()) { + if (key != null) key.cancel(); + close(); + } + } + } + + private void onReadEvent() { + var key = this.key; + try { + if (debug.on()) debug.log("onReadEvent"); + channelReadLoop(); + } finally { + if (debug.on()) { + debug.log("Leaving readEvent: ops=%s", Utils.interestOps(key)); + } + } + } + + private void onWriteEvent() { + // trigger code that will process the received + // datagrams asynchronously + // => Use a sequential scheduler, making sure it never + // runs on this thread. + // Do we need a pub/sub mechanism here? + // The write event will be paused/resumed by the + // writeLoop if needed + if (debug.on()) debug.log("onWriteEvent"); + var key = this.key; + if (key != null && key.isValid()) { + int previous; + synchronized (this) { + previous = key.interestOpsAnd(~SelectionKey.OP_WRITE); + } + if (debug.on()) debug.log("key changed from %s to: %s", + Utils.describeOps(previous), Utils.interestOps(key)); + } + writeLoopScheduler.runOrSchedule(writeLoopExecutor()); + if (debug.on() && key != null) { + debug.log("Leaving writeEvent: ops=%s", Utils.interestOps(key)); + } + } + + @Override + void writeLoop() { + super.writeLoop(); + // update selection key if needed + var key = this.key; + try { + if (key != null && key.isValid()) { + int ops, newops; + synchronized (this) { + ops = newops = key.interestOps(); + if (writeQueue.isEmpty()) { + // we have nothing else to write for now + newops &= ~SelectionKey.OP_WRITE; + } else { + // there's more to write + newops |= SelectionKey.OP_WRITE; + } + if (newops != ops && key.selector().isOpen()) { + key.interestOps(newops); + key.selector().wakeup(); + } + } + if (debug.on()) { + debug.log("leaving writeLoop: ops=%s", Utils.describeOps(newops)); + } + } + } catch (CancelledKeyException x) { + if (debug.on()) debug.log("key cancelled"); + if (writeQueue.isEmpty()) return; + else { + closeWriteQueue(x); + } + } + } + + @Override + void readLoop() { + try { + super.readLoop(); + } finally { + if (debug.on()) { + debug.log("leaving readLoop: ops=%s", Utils.interestOps(key)); + } + } + } + + @Override + public void detach() { + var key = this.key; + if (key == null) return; + if (debug.on()) { + debug.log("cancelling key: " + key); + } + // this block is needed to coordinate with attach() and + // selected(). See comment in selected(). + synchronized (this) { + key.cancel(); + this.key = null; + } + } + } + + /** + * A {@link QuicEndpoint} implementation based on blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * Virtual Threads to poll the channel. + * This implementation is tied to a {@link QuicVirtualThreadPoller}. + */ + static final class QuicVirtualThreadedEndpoint extends QuicEndpoint { + Future key; + volatile QuicVirtualThreadPoller poller; + boolean readingDone; + + private QuicVirtualThreadedEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + super(quicInstance, channel, name, timerQueue); + } + + @Override + boolean readingPaused() { + synchronized (this) { + return readingDone = super.readingPaused(); + } + } + + @Override + void resumeReading() { + boolean resumed; + boolean resumedInOtherThread = false; + QuicVirtualThreadPoller poller; + synchronized (this) { + resumed = readingStalled; + readingStalled = false; + poller = this.poller; + // readingDone is false here, it means reading already resumed + // no need to start a new reading thread + if (poller != null && (resumedInOtherThread = readingDone)) { + readingDone = false; + attach(poller); + } + } + if (resumedInOtherThread) { + // last time readingPaused() was called it returned true, so we know + // the previous poller thread has stopped reading and will exit. + // We attached a new poller above, so reading will resume in that + // other thread + // System.out.println(this + " endpoint resumed reading in new virtual thread"); + if (debug.on()) debug.log("endpoint resumed reading in new virtual thread"); + } else if (resumed) { + // readingStalled was true, and readingDone was false - which means some + // poller thread is already active, and will find readingStalled == true + // and will continue reading. So reading will resume in the currently + // active poller thread + // System.out.println(this + " endpoint resumed reading in same virtual thread"); + if (debug.on()) debug.log("endpoint resumed reading in same virtual thread"); + } // if readingStalled was false and readingDone was false there is nothing to do. + } + + @Override + void pauseReading() { + boolean paused = false; + synchronized (this) { + if (bufferTooBig()) paused = readingStalled = true; + } + if (paused) { + // System.out.println(this + " endpoint paused reading"); + if (debug.on()) debug.log("endpoint paused reading"); + } + } + + @Override + public ChannelType channelType() { + return BLOCKING_WITH_VIRTUAL_THREADS; + } + + void attach(QuicVirtualThreadPoller poller) { + this.poller = poller; + var future = poller.startReading(this); + synchronized (this) { + this.key = future; + } + } + + Executor writeLoopExecutor() { + QuicVirtualThreadPoller poller = this.poller; + if (poller == null) return executor; + return poller.readLoopExecutor(); + } + + private final SequentialScheduler channelScheduler = SequentialScheduler.lockingScheduler(this::channelReadLoop0); + + @Override + void channelReadLoop() { + channelScheduler.runOrSchedule(); + } + + private void channelReadLoop0() { + super.channelReadLoop(); + } + + @Override + public void detach() { + var key = this.key; + try { + if (key != null) { + // do not interrupt the reading task if running: + // closing the channel later on will ensure that the + // task eventually terminates. + key.cancel(false); + } + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to cancel future: " + e); + } + } + } + } + + private ByteBuffer copyOnHeap(ByteBuffer buffer) { + ByteBuffer onHeap = ByteBuffer.allocate(buffer.remaining()); + return onHeap.put(buffer).flip(); + } + + void channelReadLoop() { + // we can't prevent incoming datagram from being received + // at this level of the stack. If there is a datagram available, + // we must read it immediately and put it in the read queue. + // + // We maintain a counter of the number of bytes currently + // in the read queue. If that number exceeds a high watermark + // threshold, we will pause reading, and thus stop adding + // to the queue. + // + // As the read queue gets emptied, reading will be resumed + // when a low watermark threshold is crossed in the other + // direction. + // + // At the moment we have a single channel per endpoint, + // and we're using a single endpoint by default. + // + // We have a single selector thread, and we copy off + // the data from off-heap to on-heap before adding it + // to the queue. + // + // We can therefore do the reading directly in the + // selector thread and offload the parsing (the readLoop) + // to the executor. + // + // The readLoop will in turn resume the reading, if needed, + // when it crosses the low watermark threshold. + // + boolean nonBlocking = channelType() == NON_BLOCKING_WITH_SELECTOR; + int count; + final var buffer = this.receiveBuffer; + buffer.clear(); + final int initialStart = 1; // start readloop at first buffer + // if blocking we want to nudge the scheduler after each read since we don't + // know how much the next receive will take. If non-blocking, we nudge it + // after three consecutive read. + final int maxBeforeStart = nonBlocking ? 3 : 1; // nudge again after 3 buffers + int readLoopStarted = initialStart; + int totalpkt = 0; + try { + int sincepkt = 0; + while (!isClosed() && !readingPaused()) { + var pos = buffer.position(); + var limit = buffer.limit(); + if (debug.on()) + debug.log("receiving with buffer(pos=%s, limit=%s)", pos, limit); + assert pos == 0; + assert limit > pos; + + final SocketAddress source = channel.receive(buffer); + assert source != null || !channel.isBlocking(); + if (source == null) { + if (debug.on()) debug.log("nothing to read..."); + if (nonBlocking) break; + } + + totalpkt++; + sincepkt++; + buffer.flip(); + count = buffer.remaining(); + if (debug.on()) { + debug.log("received %s bytes from %s", count, source); + } + if (count > 0) { + // Optimization: add some basic check here to drop the packet here if: + // - it is too small, it is not a quic packet we would handle + Datagram datagram; + if ((datagram = matchDatagram(source, buffer)) == null) { + if (debug.on()) { + debug.log("dropping invalid packet for this instance (%s bytes)", count); + } + buffer.clear(); + continue; + } + // at this point buffer has been copied. We only buffer what's + // needed. + int rcv = datagram.payload().remaining(); + int buffered = buffer(rcv); + if (debug.on()) { + debug.log("adding %s in read queue from %s, queue size %s, buffered %s, type %s", + rcv, source, readQueue.size(), buffered, datagram.getClass().getSimpleName()); + } + readQueue.add(datagram); + buffer.clear(); + if (--readLoopStarted == 0 || buffered >= MAX_BUFFERED_HIGH) { + readLoopStarted = maxBeforeStart; + if (debug.on()) debug.log("triggering readLoop"); + readLoopScheduler.runOrSchedule(executor); + Deadline now; + Deadline pending; + if (nonBlocking && totalpkt > 1 && (pending = timerQueue.pendingScheduledDeadline()) + .isBefore(now = timeSource().instant())) { + // we have read 3 packets, some events are pending, return + // to the selector to process the event queue + assert this instanceof QuicEndpoint.QuicSelectableEndpoint + : "unexpected endpoint type: " + this.getClass() + "@[" + name + "]"; + assert Thread.currentThread() instanceof QuicSelector.QuicSelectorThread; + if (Log.quicRetransmit() || Log.quicTimer()) { + Log.logQuic(name() + ": reschedule needed: " + Utils.debugDeadline(now, pending) + + ", totalpkt: " + totalpkt + + ", sincepkt: " + sincepkt); + } else if (debug.on()) { + debug.log("reschedule needed: " + Utils.debugDeadline(now, pending) + + ", totalpkt: " + totalpkt + + ", sincepkt: " + sincepkt); + } + timerQueue.processEventsAndReturnNextDeadline(now, executor); + sincepkt = 0; + } + } + // check buffered.get() directly as it may have + // been decremented by the read loop already + if (this.buffered.get() >= MAX_BUFFERED_HIGH) { + // we passed the high watermark, let's pause reading. + // the read loop should already have been kicked + // of above, or will be below when we exit the while + // loop + pauseReading(); + } + } else { + if (debug.on()) debug.log("Dropped empty datagram"); + } + } + // trigger code that will process the received + // datagrams asynchronously + // => Use a sequential scheduler, making sure it never + // runs on this thread. + if (!readQueue.isEmpty() && readLoopStarted != maxBeforeStart) { + if (debug.on()) debug.log("triggering readLoop: queue size " + readQueue.size()); + readLoopScheduler.runOrSchedule(executor); + } + } catch (Throwable t) { + // TODO: special handling for interrupts? + onReadError(t); + } finally { + if (nonBlocking) { + if ((Log.quicRetransmit() && Log.channel()) || Log.quicTimer()) { + Log.logQuic(name() + ": channelReadLoop totalpkt:" + totalpkt); + } else if (debug.on()) { + debug.log("channelReadLoop totalpkt:" + totalpkt); + } + } + } + } + + /** + * This method tries to figure out whether the received packet + * matches a connection, or a stateless reset. + * @param source the source address + * @param buffer the incoming datagram payload + * @return a {@link Datagram} to be processed by the read loop + * if a match is found, or null if the datagram can be dropped + * immediately + */ + private Datagram matchDatagram(SocketAddress source, ByteBuffer buffer) { + HeadersType headersType = QuicPacketDecoder.peekHeaderType(buffer, buffer.position()); + // short header packets whose length is < 21 are never valid + if (headersType == HeadersType.SHORT && buffer.remaining() < 21) { + return null; + } + final ByteBuffer cidbytes = switch (headersType) { + case LONG, SHORT -> peekConnectionBytes(headersType, buffer); + default -> null; + }; + if (cidbytes == null) { + return null; + } + int length = cidbytes.remaining(); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + return null; + } + if (debug.on()) { + debug.log("headers(%s), connectionId(%d), datagram(%d)", + headersType, cidbytes.remaining(), buffer.remaining()); + } + QuicPacketReceiver connection = findQuicConnectionFor(source, cidbytes, headersType == HeadersType.LONG); + // check stateless reset + if (connection == null) { + if (headersType == HeadersType.SHORT) { + // a short packet may be a stateless reset, or may + // trigger a stateless reset + connection = checkStatelessReset(source, buffer); + if (connection != null) { + // We received a stateless reset, process it later in the readLoop + return new StatelessReset(connection, source, copyOnHeap(buffer)); + } else if (buffer.remaining() > 21) { + // check if we should send a stateless reset + final ByteBuffer reset = idFactory.statelessReset(cidbytes, buffer.remaining() - 1); + if (reset != null) { + // will send stateless reset later from the read loop + return new SendStatelessReset(source, reset); + } + } + return null; // drop unmatched short packets + } + // client can drop all unmatched long quic packets here + if (isClient()) return null; + } + + if (connection != null) { + if (!connection.accepts(source)) return null; + return new QuicDatagram(connection, source, copyOnHeap(buffer)); + } else { + return new UnmatchedDatagram(source, copyOnHeap(buffer)); + } + } + + + private int send(ByteBuffer datagram, SocketAddress destination) throws IOException { + return channel.send(datagram, destination); + } + + void writeLoop() { + try { + writeLoop0(); + } catch (Throwable error) { + if (!expectExceptions && !closed) { + if (Log.errors()) { + Log.logError(name + ": failed to write to channel: " + error); + Log.logError(error); + } + abort(error); + } + } + } + + boolean sendDatagram(QuicDatagram datagram) throws IOException { + int sent; + var payload = datagram.payload(); + var tosend = payload.remaining(); + final var dest = datagram.address(); + if (isClosed() || isChannelClosed()) { + if (debug.on()) { + debug.log("endpoint or channel closed; skipping sending of datagram(%d) to %s", + tosend, dest); + } + return false; + } + if (debug.on()) { + debug.log("sending datagram(%d) to %s", + tosend, dest); + } + sent = send(payload, dest); + if (debug.on()) debug.log("sent %d bytes to %s", sent, dest); + if (sent == 0 && sent != tosend) return false; + assert sent == tosend; + if (datagram.connection != null) { + datagram.connection.datagramSent(datagram); + } + return true; + } + + void onSendError(QuicDatagram datagram, int tosend, IOException x) { + // close the connection this came from? + // close all the connections whose destination is that address? + var connection = datagram.connection(); + var dest = datagram.address(); + String msg = x.getMessage(); + if (msg != null && msg.contains("too big")) { + int max = -1; + if (connection instanceof QuicConnectionImpl cimpl) { + max = cimpl.getMaxDatagramSize(); + } + msg = "on endpoint %s: Failed to send datagram (%s bytes, max: %s) to %s: %s" + .formatted(this.name, tosend, max, dest, x); + if (connection == null && debug.on()) debug.log(msg); + x = new SocketException(msg, x); + } + if (connection != null) { + connection.datagramDiscarded(datagram); + connection.onWriteError(x); + if (!channel.isOpen()) { + abort(x); + } + } + } + + private void writeLoop0() { + // write as much as we can + while (!writeQueue.isEmpty()) { + var datagram = writeQueue.peek(); + var payload = datagram.payload(); + var tosend = payload.remaining(); + try { + if (sendDatagram(datagram)) { + var rem = writeQueue.poll(); + assert rem == datagram; + } else break; + } catch (IOException x) { + // close the connection this came from? + // close all the connections whose destination is that address? + onSendError(datagram, tosend, x); + var rem = writeQueue.poll(); + assert rem == datagram; + } + } + + } + + void closeWriteQueue(Throwable t) { + QuicDatagram qd; + while ((qd = writeQueue.poll()) != null) { + if (qd.connection != null) { + qd.connection.onWriteError(t); + } + } + } + + private ByteBuffer peekConnectionBytes(HeadersType headersType, ByteBuffer payload) { + var cidlen = idFactory.connectionIdLength(); + return switch (headersType) { + case LONG -> QuicPacketDecoder.peekLongConnectionId(payload); + case SHORT -> QuicPacketDecoder.peekShortConnectionId(payload, cidlen); + default -> null; + }; + } + + // The readloop is triggered whenever new datagrams are + // added to the read queue. + void readLoop() { + try { + if (debug.on()) debug.log("readLoop"); + while (!readQueue.isEmpty()) { + var datagram = readQueue.poll(); + var payload = datagram.payload(); + var source = datagram.address(); + int remaining = payload.remaining(); + var pos = payload.position(); + unbuffer(remaining); + if (debug.on()) { + debug.log("readLoop: type(%x) %d from %s", + payload.hasRemaining() ? payload.get(0) : 0, + remaining, + source); + } + try { + if (closed) { + if (debug.on()) { + debug.log("closed: ignoring incoming datagram"); + } + return; + } + switch (datagram) { + case QuicDatagram(var connection, var _, var _) -> { + var headersType = QuicPacketDecoder.peekHeaderType(payload, pos); + var destConnId = peekConnectionBytes(headersType, payload); + connection.processIncoming(source, destConnId, headersType, payload); + } + case UnmatchedDatagram(var _, var _) -> { + var headersType = QuicPacketDecoder.peekHeaderType(payload, pos); + unmatchedQuicPacket(datagram, headersType, payload); + } + case StatelessReset(var connection, var _, var _) -> { + if (debug.on()) { + debug.log("Processing stateless reset from %s", source); + } + connection.processStatelessReset(); + } + case SendStatelessReset(var _, var _) -> { + if (debug.on()) { + debug.log("Sending stateless reset to %s", source); + } + send(payload, source); + } + } + + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to handle datagram: " + t, t); + Log.logError(t); + } + } + } catch (Throwable t) { + onReadError(t); + } + } + + private void onReadError(Throwable t) { + if (!expectExceptions) { + if (debug.on()) { + debug.log("Error handling event: ", t); + } + Log.logError(t); + if (t instanceof RejectedExecutionException + || t instanceof ClosedChannelException + || t instanceof AssertionError) { + expectExceptions = true; + abort(t); + } + } + } + + /** + * checks if the received datagram contains a stateless reset token; + * returns the associated connection if true, null otherwise + * @param source the sender's address + * @param buffer datagram contents + * @return connection associated with the stateless token, or {@code null} + */ + protected QuicPacketReceiver checkStatelessReset(SocketAddress source, final ByteBuffer buffer) { + // We couldn't identify the connection: maybe that's a stateless reset? + if (closed) return null; + if (debug.on()) { + debug.log("Check if received datagram could be stateless reset (datagram[%d, %s])", + buffer.remaining(), source); + } + if (buffer.remaining() < 21) { + // too short to be a stateless reset: + // RFC 9000: + // Endpoints MUST discard packets that are too small to be valid QUIC packets. + // To give an example, with the set of AEAD functions defined in [QUIC-TLS], + // short header packets that are smaller than 21 bytes are never valid. + if (debug.on()) { + debug.log("Packet too short for a stateless reset (%s bytes < 21)", + buffer.remaining()); + } + return null; + } + final byte[] tokenBytes = new byte[16]; + buffer.get(buffer.limit() - 16, tokenBytes); + final var token = makeToken(tokenBytes); + QuicPacketReceiver connection = peerIssuedResetTokens.get(token); + if (closed) return null; + if (connection != null) { + if (debug.on()) { + debug.log("Received reset token: %s for connection: %s", + HexFormat.of().formatHex(tokenBytes), connection); + } + } else { + if (debug.on()) { + debug.log("Not a stateless reset"); + } + } + return connection; + } + + private StatelessResetToken makeToken(byte[] tokenBytes) { + // encrypt token to block timing attacks, see RFC 9000 section 10.3.1 + try { + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, tokenEncryptionKey); + byte[] encryptedBytes = cipher.doFinal(tokenBytes); + return new StatelessResetToken(encryptedBytes); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | + IllegalBlockSizeException | BadPaddingException | + InvalidKeyException e) { + throw new RuntimeException("AES encryption failed", e); + } + } + + /** + * Called when parsing a quic packet that couldn't be matched to any registered + * connection. + * + * @param datagram The datagram containing the packet + * @param headersType The quic packet type + * @param buffer A buffer positioned at the start of the unmatched quic packet. + * The buffer may contain more coalesced quic packets. + */ + protected void unmatchedQuicPacket(Datagram datagram, + HeadersType headersType, + ByteBuffer buffer) throws IOException { + QuicInstance instance = quicInstance; + if (closed) { + if (debug.on()) { + debug.log("closed: ignoring unmatched datagram"); + } + return; + } + + var address = datagram.address(); + if (isServer() && headersType == HeadersType.LONG ) { + // long packets need to be rematched here for servers. + // we read packets in one thread and process them here in + // a different thread: + // the connection may have been added later on when processing + // a previous long packet in this thread, so we need to + // check the connection map again here. + var idbytes = peekConnectionBytes(headersType, buffer); + var connection = findQuicConnectionFor(address, idbytes, true); + if (connection != null) { + // a matching connection was found, this packet is no longer + // unmatched + if (connection.accepts(address)) { + connection.processIncoming(address, idbytes, headersType, buffer); + } + return; + } + } + + if (debug.on()) { + debug.log("Unmatched packet in datagram [%s, %d, %s] for %s", headersType, + buffer.remaining(), address, instance); + debug.log("Unmatched packet: delegating to instance"); + } + instance.unmatchedQuicPacket(address, headersType, buffer); + } + + private boolean isServer() { + return !isClient(); + } + + private boolean isClient() { + return quicInstance instanceof QuicClient; + } + + // Parses the list of active connection + // Attempts to find one that matches + // If none match return null + // Revisit: + // if we had an efficient sorted tree where we could locate a connection id + // from the idbytes we wouldn't need to use an "unsafe connection id" + // quick and dirty solution for now: we use a ConcurrentHashMap and construct + // a throw away QuicConnectionId that wrap our mutable idbytes. + // This is OK since the classes that may see these bytes are all internal + // and won't mutate them. + QuicPacketReceiver findQuicConnectionFor(SocketAddress peerAddress, ByteBuffer idbytes, boolean longHeaders) { + if (idbytes == null) return null; + var cid = idFactory.unsafeConnectionIdFor(idbytes); + if (cid == null) { + if (!longHeaders || isClient()) { + if (debug.on()) { + debug.log("No connection match for: %s", Utils.asHexString(idbytes)); + } + return null; + } + // this is a long headers packet and we're the server; + // the client might still be using the original connection ID + cid = new PeerConnectionId(idbytes, null); + } + if (debug.on()) { + debug.log("Looking up QuicConnection for: %s", cid); + } + var quicConnection = connections.get(cid); + assert quicConnection == null || allConnectionIds(quicConnection).anyMatch(cid::equals); + return quicConnection; + } + + private static Stream allConnectionIds(QuicPacketReceiver quicConnection) { + return Stream.concat(quicConnection.connectionIds().stream(), quicConnection.initialConnectionId().stream()); + } + + /** + * Detach the channel from the selector implementation + */ + public abstract void detach(); + + private void silentTerminateConnection(QuicPacketReceiver c) { + try { + if (c instanceof QuicConnectionImpl connection) { + final TerminationCause st = forSilentTermination("QUIC endpoint closed - no error"); + connection.terminator.terminate(st); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close connection %s: %s", c, t); + } + } finally { + if (c != null) c.shutdown(); + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + void abortConnection(QuicPacketReceiver c, Throwable error) { + try { + if (c instanceof QuicConnectionImpl connection) { + connection.terminator.terminate(TerminationCause.forException(error)); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close connection %s: %s", c, t); + } + } finally { + if (c != null) c.shutdown(); + } + } + + boolean isClosed() { + return closed; + } + + private void detachAndCloseChannel() throws IOException { + try { + detach(); + } finally { + channel.close(); + } + } + + volatile boolean expectExceptions; + + @Override + public void close() { + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + } + try { + while (!connections.isEmpty()) { + if (debug.on()) { + debug.log("closing %d connections", connections.size()); + } + final Set connCloseSent = new HashSet<>(); + for (var cid : connections.keySet()) { + // endpoint is closing, so (on a best-effort basis) we send out a datagram + // containing a QUIC packet with a CONNECTION_CLOSE frame to the peer. + // Immediately after that, we silently terminate the connection since + // there's no point maintaining the connection's infrastructure for + // sending (or receiving) additional packets when the endpoint itself + // won't be around for dealing with the packets. + final QuicPacketReceiver rcvr = connections.remove(cid); + if (rcvr instanceof QuicConnectionImpl quicConn) { + final boolean shouldSendConnClose = connCloseSent.add(quicConn); + // send the datagram containing the CONNECTION_CLOSE frame only once + // per connection + if (shouldSendConnClose) { + sendConnectionCloseQuietly(quicConn); + } + } + silentTerminateConnection(rcvr); + } + } + } finally { + try { + // TODO: do we need to wait for something (ACK?) + // before actually stopping all loop and closing the channel? + if (debug.on()) { + debug.log("Closing channel " + channel + " of endpoint " + this); + } + writeLoopScheduler.stop(); + readLoopScheduler.stop(); + QuicDatagram datagram; + while ((datagram = writeQueue.poll()) != null) { + if (datagram.connection != null) { + datagram.connection.datagramDropped(datagram); + } + } + expectExceptions = true; + detachAndCloseChannel(); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to detach and close channel: " + io); + } + } + } + + // sends a datagram with a CONNECTION_CLOSE frame for the connection and ignores + // any exceptions that may occur while trying to do so. + private void sendConnectionCloseQuietly(final QuicConnectionImpl quicConn) { + try { + final Optional datagram = quicConn.connectionCloseDatagram(); + if (datagram.isEmpty()) { + return; + } + if (debug.on()) { + debug.log("sending CONNECTION_CLOSE datagram for connection %s", quicConn); + } + send(datagram.get().payload(), datagram.get().address()); + } catch (Exception e) { + // ignore + if (debug.on()) { + debug.log("failed to send CONNECTION_CLOSE datagram for" + + " connection %s due to %s", quicConn, e); + } + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable error) { + + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + } + assert closed; + if (debug.on()) { + debug.log("aborting: " + error); + } + writeLoopScheduler.stop(); + readLoopScheduler.stop(); + QuicDatagram datagram; + while ((datagram = writeQueue.poll()) != null) { + if (datagram.connection != null) { + datagram.connection.datagramDropped(datagram); + } + } + try { + while (!connections.isEmpty()) { + if (debug.on()) + debug.log("closing %d connections", connections.size()); + for (var cid : connections.keySet()) { + abortConnection(connections.remove(cid), error); + } + } + } finally { + try { + if (debug.on()) { + debug.log("Closing channel " + channel + " of endpoint " + this); + } + detachAndCloseChannel(); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to detach and close channel: " + io); + } + } + } + + + @Override + public String toString() { + return name; + } + + boolean forceSendAsync() { + return DGRAM_SEND_ASYNC || !writeQueue.isEmpty(); + // TODO remove + // perform all writes in a virtual thread. This should trigger + // JDK-8334574 more frequently. + // || (IS_WINDOWS + // && channelType().isBlocking() + // && !Thread.currentThread().isVirtual()); + } + + /** + * Schedule a datagram for writing to the underlying channel. + * If any datagram is pending the given datagram is appended + * to the list of pending datagrams for writing. + * @param source the source connection + * @param destination the destination address + * @param payload the encrypted datagram + */ + public void pushDatagram(QuicPacketReceiver source, SocketAddress destination, ByteBuffer payload) { + int tosend = payload.remaining(); + if (debug.on()) { + debug.log("attempting to send datagram [%s bytes]", tosend); + } + var datagram = new QuicDatagram(source, destination, payload); + try { + // if DGRAM_SEND_ASYNC is true we don't attempt to send from the current + // thread but push the datagram on the queue and invoke the write loop. + if (forceSendAsync() || !sendDatagram(datagram)) { + if (tosend == payload.remaining()) { + writeQueue.add(datagram); + if (debug.on()) { + debug.log("datagram [%s bytes] added to write queue, queue size %s", + tosend, writeQueue.size()); + } + writeLoopScheduler.runOrSchedule(writeLoopExecutor()); + } else { + source.datagramDropped(datagram); + if (debug.on()) { + debug.log("datagram [%s bytes] dropped: payload partially consumed, remaining %s", + tosend, payload.remaining()); + } + } + } + } catch (IOException io) { + onSendError(datagram, tosend, io); + } + } + + /** + * Called to schedule sending of a datagram that contains a {@code ConnectionCloseFrame}. + * This will replace the {@link QuicConnectionImpl} with a {@link ClosedConnection} that + * will replay the datagram containing the {@code ConnectionCloseFrame} whenever a packet + * for that connection is received. + * @param connection the connection being closed + * @param destination the peer address + * @param datagram the datagram + */ + public void pushClosingDatagram(QuicConnectionImpl connection, InetSocketAddress destination, ByteBuffer datagram) { + if (debug.on()) debug.log("Pushing closing datagram for " + connection.logTag()); + closing(connection, datagram.slice()); + pushDatagram(connection, destination, datagram); + } + + /** + * Called to schedule sending of a datagram that contains a single {@code ConnectionCloseFrame} + * sent in response to a {@code ConnectionClose} frame. + * This will completely remove the connection from the connection map. + * @param connection the connection being closed + * @param destination the peer address + * @param datagram the datagram + */ + public void pushClosedDatagram(QuicConnectionImpl connection, + InetSocketAddress destination, + ByteBuffer datagram) { + if (debug.on()) debug.log("Pushing closed datagram for " + connection.logTag()); + removeConnection(connection); + pushDatagram(connection, destination, datagram); + } + + /** + * This will completely remove the connection from the endpoint. Any subsequent packets + * directed to this connection from a peer, may end up receiving a stateless reset + * from this endpoint. + * + * @param connection the connection to be removed + */ + void removeConnection(final QuicPacketReceiver connection) { + if (debug.on()) debug.log("removing connection " + connection); + // remove the connection completely + connection.connectionIds().forEach(connections::remove); + assert !connections.containsValue(connection) : connection; + // remove references to this connection from the map which holds the peer issued + // reset tokens + dropPeerIssuedResetTokensFor(connection); + } + + /** + * Add the cid to connection mapping to the endpoint. + * + * @param cid the connection ID to be added + * @param connection the connection that should be mapped to the cid + * @return true if connection ID was added, false otherwise + */ + public boolean addConnectionId(QuicConnectionId cid, QuicPacketReceiver connection) { + var old = connections.putIfAbsent(cid, connection); + return old == null; + } + + /** + * Remove the cid to connection mapping from the endpoint. + * + * @param cid the connection ID to be removed + * @param connection the connection that is mapped to the cid + * @return true if connection ID was removed, false otherwise + */ + public boolean removeConnectionId(QuicConnectionId cid, QuicPacketReceiver connection) { + if (debug.on()) debug.log("removing connection ID " + cid); + return connections.remove(cid, connection); + } + + public final int connectionCount() { + return connections.size(); + } + + // drop peer issued stateless tokes for the given connection + private void dropPeerIssuedResetTokensFor(QuicPacketReceiver connection) { + // remove references to this connection from the map which holds the peer issued + // reset tokens + peerIssuedResetTokens.values().removeIf(conn -> connection == conn); + } + + // remap peer issued stateless token from connection `from` to connection `to` + private void remapPeerIssuedResetToken(QuicPacketReceiver from, QuicPacketReceiver to) { + assert from != null; + assert to != null; + peerIssuedResetTokens.replaceAll((tok, c) -> c == from ? to : c); + } + + public void draining(final QuicPacketReceiver connection) { + // remap the connection to a DrainingConnection + if (closed) return; + connection.connectionIds().forEach((id) -> + connections.compute(id, this::remapDraining)); + assert !connections.containsValue(connection) : connection; + } + + private DrainingConnection remapDraining(QuicConnectionId id, QuicPacketReceiver conn) { + if (closed) return null; + var debugOn = debug.on() && !Thread.currentThread().isVirtual(); + if (conn instanceof ClosingConnection closing) { + if (debugOn) debug.log("remapping %s to DrainingConnection", id); + final var draining = closing.toDraining(); + remapPeerIssuedResetToken(closing, draining); + draining.startTimer(); + return draining; + } else if (conn instanceof DrainingConnection draining) { + return draining; + } else if (conn instanceof QuicConnectionImpl impl) { + final long idleTimeout = impl.peerPtoMs() * 3; // 3 PTO + impl.localConnectionIdManager().close(); + if (debugOn) debug.log("remapping %s to DrainingConnection", id); + var draining = new DrainingConnection(conn.connectionIds(), idleTimeout); + // we can ignore stateless reset in the draining state. + remapPeerIssuedResetToken(impl, draining); + draining.startTimer(); + return draining; + } else if (conn == null) { + // connection absent (was probably removed), don't remap to draining + if (debugOn) { + debug.log("no existing connection present for %s, won't remap to draining", id); + } + return null; + } else { + assert false : "unexpected connection type: " + conn; // just remove + return null; + } + } + + protected void closing(QuicConnectionImpl connection, ByteBuffer datagram) { + if (closed) return; + ByteBuffer closing = ByteBuffer.allocate(datagram.limit()); + closing.put(datagram.slice()); + closing.flip(); + connection.connectionIds().forEach((id) -> + connections.compute(id, (i, r) -> remapClosing(i, r, closing))); + assert !connections.containsValue(connection) : connection; + } + + private ClosedConnection remapClosing(QuicConnectionId id, QuicPacketReceiver conn, ByteBuffer datagram) { + if (closed) return null; + var debugOn = debug.on() && !Thread.currentThread().isVirtual(); + if (conn instanceof ClosingConnection closing) { + // we already have a closing datagram, drop the new one + return closing; + } else if (conn instanceof DrainingConnection draining) { + return draining; + } else if (conn instanceof QuicConnectionImpl impl) { + final long idleTimeout = impl.peerPtoMs() * 3; // 3 PTO + impl.localConnectionIdManager().close(); + if (debugOn) debug.log("remapping %s to ClosingConnection", id); + var closing = new ClosingConnection(conn.connectionIds(), idleTimeout, datagram); + remapPeerIssuedResetToken(impl, closing); + closing.startTimer(); + return closing; + } else if (conn == null) { + // connection absent (was probably removed), don't remap to closing + if (debugOn) { + debug.log("no existing connection present for %s, won't remap to closing", id); + } + return null; + } else { + assert false : "unexpected connection type: " + conn; // just remove + return null; + } + } + + public void registerNewConnection(QuicConnectionImpl quicConnection) throws IOException { + if (closed) throw new ClosedChannelException(); + quicConnection.connectionIds().forEach((id) -> putConnection(id, quicConnection)); + } + + /** + * A peer issues a stateless reset token which it can then send to close the connection. This + * method links the peer issued token against the connection that needs to be closed if/when + * that stateless reset token arrives in the packet. + * + * @param statelessResetToken the peer issued (16 byte) stateless reset token + * @param connection the connection to link the token against + */ + void associateStatelessResetToken(final byte[] statelessResetToken, final QuicPacketReceiver connection) { + Objects.requireNonNull(connection); + Objects.requireNonNull(statelessResetToken); + final int tokenLength = statelessResetToken.length; + if (statelessResetToken.length != 16) { + throw new IllegalArgumentException("Invalid stateless reset token length " + tokenLength); + } + if (debug.on()) { + debug.log("associating stateless reset token with connection %s", connection); + } + this.peerIssuedResetTokens.put(makeToken(statelessResetToken), connection); + } + + /** + * Discard the stateless reset token that this endpoint might have previously + * {@link #associateStatelessResetToken(byte[], QuicPacketReceiver) associated any connection} + * with + * @param statelessResetToken The stateless reset token + */ + void forgetStatelessResetToken(final byte[] statelessResetToken) { + // just a tiny optimization - we know stateless reset token must be of 16 bytes, if the passed + // value isn't, then no point doing any more work + if (statelessResetToken.length != 16) { + return; + } + this.peerIssuedResetTokens.remove(makeToken(statelessResetToken)); + } + + /** + * {@return the timer queue associated with this endpoint} + */ + public QuicTimerQueue timer() { + return timerQueue; + } + + public boolean isChannelClosed() { + return !channel().isOpen(); + } + + /** + * {@return the time source associated with this endpoint} + * @apiNote + * There is a unique global {@linkplain TimeSource#source()} for the whole + * JVM, but this method can be overridden in tests to define an alternative + * timeline for the test. + */ + protected TimeLine timeSource() { + return TimeSource.source(); + } + + private void putConnection(QuicConnectionId id, QuicConnectionImpl quicConnection) { + // ideally we'd want to use an immutable byte buffer as a key here. + // but we don't have that. So we use the connection id instead. + var old = connections.put(id, quicConnection); + assert old == null : "%s already registered with %s (%s)" + .formatted(old, id, old == quicConnection ? "old == new" : "old != new"); + } + + + /** + * Represent a closing or draining quic connection: if we receive any packet + * for this connection we ignore them (if in draining state) or replay the + * closed packets in decreasing frequency: we reply to the + * first packet, then to the third, then to the seventh, etc... + * We stop replying after 16*16/2. + */ + sealed abstract class ClosedConnection implements QuicPacketReceiver, QuicTimedEvent + permits QuicEndpoint.ClosingConnection, QuicEndpoint.DrainingConnection { + + // default time we keep the ClosedConnection alive while closing/draining - if + // PTO information is not available (if 0 is passed as idleTimeoutMs when creating + // an instance of this class) + final static long NO_IDLE_TIMEOUT = 2000; + final List localConnectionIds; + final long maxIdleTimeMs; + final long id; + int more = 1; + int waitformore; + volatile Deadline deadline; + volatile Deadline updatedDeadline; + + ClosedConnection(List localConnectionIds, long maxIdleTimeMs) { + this.id = QuicTimerQueue.newEventId(); + this.maxIdleTimeMs = maxIdleTimeMs == 0 ? NO_IDLE_TIMEOUT : maxIdleTimeMs; + this.deadline = Deadline.MAX; + this.updatedDeadline = Deadline.MAX; + this.localConnectionIds = List.copyOf(localConnectionIds); + } + + @Override + public List connectionIds() { + return localConnectionIds; + } + + @Override + public final void processIncoming(SocketAddress source, ByteBuffer destConnId, HeadersType headersType, ByteBuffer buffer) { + Deadline updated = updatedDeadline; + var waitformore = this.waitformore; + // Deadline.MIN will be set in case of write errors + if (updated != Deadline.MIN && waitformore == 0) { + var more = this.more; + this.waitformore = more; + this.more = more = more << 1; + if (more > 16) { + // the server doesn't seem to take into account our + // connection close frame. Just stop responding + updatedDeadline = Deadline.MIN; + } else { + updatedDeadline = updated.plusMillis(maxIdleTimeMs); + } + handleIncoming(source, destConnId, headersType, buffer); + } else { + this.waitformore = waitformore - 1; + dropIncoming(source, destConnId, headersType, buffer); + } + + timer().reschedule(this, updatedDeadline); + } + + protected void handleIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer) { + dropIncoming(source, idbytes, headersType, buffer); + } + + protected abstract void dropIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer); + + @Override + public final void onWriteError(Throwable t) { + if (debug.on()) + debug.log("failed to write close packet", t); + removeConnection(this); + // handle() will be called, which will cause + // the timer queue to remove this object + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + + public final void startTimer() { + deadline = updatedDeadline = timeSource().instant().plusMillis(maxIdleTimeMs); + timer().offer(this); + } + + @Override + public final Deadline deadline() { + return deadline; + } + + @Override + public final Deadline handle() { + removeConnection(this); + // Deadline.MAX means do not reschedule + return updatedDeadline = Deadline.MAX; + } + + @Override + public final Deadline refreshDeadline() { + // Returning Deadline.MIN here will cause handle() to + // be called and will remove this task from the timer queue. + return deadline = updatedDeadline; + } + + @Override + public final long eventId() { + return id; + } + + @Override + public final void processStatelessReset() { + // the peer has sent us a stateless reset: no need to + // replay CloseConnectionFrame. Just remove this connection. + removeConnection(this); + // handle() will be called, which will cause + // the timer queue to remove this object + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + + public void shutdown() { + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + } + + + /** + * Represent a closing quic connection: if we receive any packet for this + * connection we simply replay the packet(s) that contained the + * ConnectionCloseFrame frame. + * Packets are replayed in decreasing frequency. We reply to the + * first packet, then to the third, then to the seventh, etc... + * We stop replying after 16*16/2. + */ + final class ClosingConnection extends ClosedConnection { + + final ByteBuffer closePacket; + + ClosingConnection(List localConnIdManager, long maxIdleTimeMs, + ByteBuffer closePacket) { + super(localConnIdManager, maxIdleTimeMs); + this.closePacket = Objects.requireNonNull(closePacket); + } + + @Override + public void handleIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer) { + if (isClosed() || isChannelClosed()) { + // don't respond with any more datagrams and instead just drop + // the incoming ones since the channel is closed + dropIncoming(source, idbytes, headersType, buffer); + return; + } + if (debug.on()) { + debug.log("ClosingConnection(%s): sending closed packets", localConnectionIds); + } + pushDatagram(this, source, closePacket.asReadOnlyBuffer()); + } + + @Override + protected void dropIncoming(SocketAddress source, ByteBuffer idbytes, HeadersType headersType, ByteBuffer buffer) { + if (debug.on()) { + debug.log("ClosingConnection(%s): dropping %s packet", localConnectionIds, headersType); + } + } + + private DrainingConnection toDraining() { + return new DrainingConnection(localConnectionIds, maxIdleTimeMs); + } + } + + /** + * Represent a draining quic connection: if we receive any packet for this + * connection we simply ignore them. + */ + final class DrainingConnection extends ClosedConnection { + + DrainingConnection(List localConnIdManager, long maxIdleTimeMs) { + super(localConnIdManager, maxIdleTimeMs); + } + + @Override + public void dropIncoming(SocketAddress source, ByteBuffer idbytes, HeadersType headersType, ByteBuffer buffer) { + if (debug.on()) { + debug.log("DrainingConnection(%s): dropping %s packet", + localConnectionIds, headersType); + } + } + + } + + private record StatelessResetToken (byte[] token) { + StatelessResetToken(final byte[] token) { + this.token = token.clone(); + } + @Override + public int hashCode() { + return Arrays.hashCode(token); + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof StatelessResetToken other) { + return Arrays.equals(token, other.token); + } + return false; + } + } + + /** + * {@return a new {@link QuicEndpoint} of the given {@code endpointType}} + * @param endpointType the concrete endpoint type, one of {@link QuicSelectableEndpoint + * QuicSelectableEndpoint.class} or {@link QuicVirtualThreadedEndpoint + * QuicVirtualThreadedEndpoint.class}. + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @param the concrete endpoint type, one of {@link QuicSelectableEndpoint} + * or {@link QuicVirtualThreadedEndpoint} + * @throws IOException if an IOException occurs + * @throws IllegalArgumentException if the given endpoint type is not one of + * {@link QuicSelectableEndpoint QuicSelectableEndpoint.class} or + * {@link QuicVirtualThreadedEndpoint QuicVirtualThreadedEndpoint.class} + */ + private static T create(Class endpointType, + QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) throws IOException { + DatagramChannel channel = DatagramChannel.open(); + // avoid dependency on extnet + Optional> df = channel.supportedOptions().stream(). + filter(o -> "IP_DONTFRAGMENT".equals(o.name())).findFirst(); + if (df.isPresent()) { + // TODO on some platforms this doesn't work on dual stack sockets + // see Net#shouldSetBothIPv4AndIPv6Options + @SuppressWarnings("unchecked") + var option = (SocketOption) df.get(); + channel.setOption(option, true); + } + if (QuicSelectableEndpoint.class.isAssignableFrom(endpointType)) { + channel.configureBlocking(false); + } + Consumer logSink = Log.quic() ? Log::logQuic : null; + Utils.configureChannelBuffers(logSink, channel, + quicInstance.getReceiveBufferSize(), quicInstance.getSendBufferSize()); + channel.bind(bindAddress); // could do that on attach instead? + + if (endpointType.isAssignableFrom(QuicSelectableEndpoint.class)) { + return endpointType.cast(new QuicSelectableEndpoint(quicInstance, channel, name, timerQueue)); + } else if (endpointType.isAssignableFrom(QuicVirtualThreadedEndpoint.class)) { + return endpointType.cast(new QuicVirtualThreadedEndpoint(quicInstance, channel, name, timerQueue)); + } else { + throw new IllegalArgumentException(endpointType.getName()); + } + } + + public static final class QuicEndpointFactory { + + public QuicEndpointFactory() { + } + + /** + * {@return a new {@code QuicSelectableEndpoint}} + * + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @throws IOException if an IOException occurs + */ + public QuicSelectableEndpoint createSelectableEndpoint(QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) + throws IOException { + return create(QuicSelectableEndpoint.class, quicInstance, name, bindAddress, timerQueue); + } + + /** + * {@return a new {@code QuicVirtualThreadedEndpoint}} + * + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @throws IOException if an IOException occurs + */ + public QuicVirtualThreadedEndpoint createVirtualThreadedEndpoint(QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) + throws IOException { + return create(QuicVirtualThreadedEndpoint.class, quicInstance, name, bindAddress, timerQueue); + } + } + + /** + * Registers the given endpoint with the given selector. + *

    + * An endpoint of class {@link QuicSelectableEndpoint} is only + * compatible with a selector of type {@link QuicNioSelector}. + * An endpoint of tyoe {@link QuicVirtualThreadedEndpoint} is only + * compatible with a selector of type {@link QuicVirtualThreadPoller}. + *
    + * If the given endpoint implementation is not compatible with + * the given selector implementation an {@link IllegalStateException} + * is thrown. + * + * @param endpoint the endpoint + * @param selector the selector + * @param debug a logger for debugging + * + * @throws IOException if an IOException occurs + * @throws IllegalStateException if the endpoint and selector implementations + * are not compatible + */ + public static void registerWithSelector(QuicEndpoint endpoint, QuicSelector selector, Logger debug) + throws IOException { + if (selector instanceof QuicVirtualThreadPoller poller) { + var loopingEndpoint = (QuicVirtualThreadedEndpoint) endpoint; + poller.register(loopingEndpoint); + } else if (selector instanceof QuicNioSelector selectable) { + var selectableEndpoint = (QuicEndpoint.QuicSelectableEndpoint) endpoint; + selectable.register(selectableEndpoint); + } else { + throw new IllegalStateException("Incompatible selector and endpoint implementations: %s <-> %s" + .formatted(selector.getClass(), endpoint.getClass())); + } + if (debug.on()) debug.log("endpoint registered with selector"); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java new file mode 100644 index 00000000000..a963625d7f5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.HexFormat; +import java.util.List; +import java.util.concurrent.Executor; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +/** + * A {@code QuicInstance} represents a common abstraction which is + * either a {@code QuicClient} or a {@code QuicServer}, or possibly + * both. It defines the subset of public methods that a + * {@code QuicEndpoint} and a {@code QuicSelector} need to operate + * with a quic client, or a quic server; + */ +public interface QuicInstance { + + /** + * The executor used by this quic instance when a task needs to + * be offloaded to a separate thread. + * @implNote This is the HttpClientImpl internal executor. + * @return the executor used by this QuicClient. + */ + Executor executor(); + + /** + * {@return an endpoint to associate with a connection} + * @throws IOException + */ + QuicEndpoint getEndpoint() throws IOException; + + /** + * This method is called when a quic packet that couldn't be attributed + * to a registered connection is received. + * @param source the source address of the datagram + * @param type the packet type + * @param buffer A buffer positioned at the start of the quic packet + */ + void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer); + + /** + * {@return true if the passed version is available for use on this instance, false otherwise} + */ + boolean isVersionAvailable(QuicVersion quicVersion); + + /** + * {@return the versions that are available for use on this instance} + */ + List getAvailableVersions(); + + /** + * Instance ID used for debugging traces. + * @return A string uniquely identifying this instance. + */ + String instanceId(); + + /** + * Get the QuicTLSContext used by this quic instance. + * @return the QuicTLSContext used by this quic instance. + */ + QuicTLSContext getQuicTLSContext(); + + QuicTransportParameters getTransportParameters(); + + /** + * The {@link SSLParameters} for this Quic instance. + * May be {@code null} if no parameters have been specified. + * + * @implSpec + * The default implementation of this method returns {@code null}. + * + * @return The {@code SSLParameters} for this quic instance or {@code null}. + */ + default SSLParameters getSSLParameters() { return new SSLParameters(); } + + /** + * {@return the configured {@linkplain java.net.StandardSocketOptions#SO_RCVBUF + * UDP receive buffer} size this instance should use} + */ + default int getReceiveBufferSize() { + return Utils.getIntegerNetProperty( + "jdk.httpclient.quic.receiveBufferSize", + 0 // only set the size if > 0 + ); + } + + /** + * {@return the configured {@linkplain java.net.StandardSocketOptions#SO_SNDBUF + * UDP send buffer} size this instance should use} + */ + default int getSendBufferSize() { + return Utils.getIntegerNetProperty( + "jdk.httpclient.quic.sendBufferSize", + 0 // only set the size if > 0 + ); + } + + /** + * {@return a string describing the given application error code} + * @param errorCode an application error code + * @implSpec By default, this method returns a generic + * string containing the hexadecimal value of the given errorCode. + * Subclasses built for supporting a given application protocol, + * such as HTTP/3, may override this method to return more + * specific names, such as for instance, {@code "H3_REQUEST_CANCELLED"} + * for {@code 0x010c}. + * @apiNote This method is typically used for logging and/or debugging + * purposes, to generate a more user-friendly log message. + */ + default String appErrorToString(long errorCode) { + return "ApplicationError(code=0x" + HexFormat.of().toHexDigits(errorCode) + ")"; + } + + default String name() { + return String.format("%s(%s)", this.getClass().getSimpleName(), instanceId()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java new file mode 100644 index 00000000000..6652b44de3a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.quic.QuicEndpoint.QuicDatagram; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Optional; + +/** + * The {@code QuicPacketReceiver} is an abstraction that defines the + * interface between a {@link QuicEndpoint} and a {@link QuicConnection}. + * This defines the minimum set of methods that the endpoint will need + * in order to be able to dispatch a received {@link jdk.internal.net.http.quic.packets.QuicPacket} + * to its destination. This abstraction is typically useful when dealing with + * {@linkplain QuicEndpoint.ClosedConnection + * closed connections, which need to remain alive for a certain time + * after being closed in order to satisfy the requirement of the quic + * protocol (typically for retransmitting the CLOSE_CONNECTION frame + * if needed). + */ +public interface QuicPacketReceiver { + + /** + * {@return a list of local connectionIds for this connection) + */ + List connectionIds(); + + /** + * {@return the initial connection id assigned by the peer} + * On the client side, this is always {@link Optional#empty()}. + * On the server side, it contains the initial connection id + * that was assigned by the client in the first INITIAL packet. + * + * @implSpec + * The default implementation of this method returns {@link Optional#empty()} + */ + default Optional initialConnectionId() { + return Optional.empty(); + } + + /** + * Called when an incoming datagram is received. + *

    + * The buffer is positioned at the start of the datagram to process. + * The buffer may contain more than one QUIC packet. + * + * @param source The peer address, as received from the UDP stack + * @param destConnId Destination connection id bytes included in the packet + * @param headersType The quic packet type + * @param buffer A buffer positioned at the start of the quic packet, + * not yet decrypted, and possibly containing coalesced + * packets. + */ + void processIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer); + + /** + * Called when a datagram scheduled for writing by this connection + * could not be written to the network. + * @param t the error that occurred + */ + void onWriteError(Throwable t); + + /** + * Called when a stateless reset token is received. + */ + void processStatelessReset(); + + /** + * Called to shut a closed connection down. + * This is the last step when closing a connection, and typically + * only release resources held by all packet spaces. + */ + void shutdown(); + + /** + * Called after a datagram has been written to the socket. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was sent + */ + default void datagramSent(QuicDatagram datagram) { } + + /** + * Called after a datagram has been discarded as a result of + * some error being raised, for instance, when an attempt + * to write it to the socket has failed, or if the encryption + * of a packet in the datagram has failed. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was discarded + */ + default void datagramDiscarded(QuicDatagram datagram) { } + + /** + * Called after a datagram has been dropped. Typically, this + * could happen if the datagram was only partly written, or if + * the connection was closed before the datagram could be sent. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was dropped + */ + default void datagramDropped(QuicDatagram datagram) { } + + /** + * {@return whether this receiver accepts packets from the given source} + * @param source the sender address + */ + default boolean accepts(SocketAddress source) { + return true; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java new file mode 100644 index 00000000000..fde253740d1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Implementation of QUIC congestion controller based on RFC 9002. + * This is a QUIC variant of New Reno algorithm. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +class QuicRenoCongestionController implements QuicCongestionController { + // higher of 14720 and 2*maxDatagramSize; we use fixed maxDatagramSize + private static final int INITIAL_WINDOW = Math.max(14720, 2 * QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE); + private static final int MAX_BYTES_IN_FLIGHT = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxBytesInFlight", 1 << 24), + 1 << 14, 1 << 24); + private final TimeLine timeSource; + private final String dbgTag; + private final Lock lock = new ReentrantLock(); + private long congestionWindow = INITIAL_WINDOW; + private int maxDatagramSize = QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE; + private int minimumWindow = 2 * maxDatagramSize; + private long bytesInFlight; + // maximum bytes in flight seen since the last congestion event + private long maxBytesInFlight; + private Deadline congestionRecoveryStartTime; + private long ssThresh = Long.MAX_VALUE; + + public QuicRenoCongestionController(String dbgTag) { + this.dbgTag = dbgTag; + this.timeSource = TimeSource.source(); + } + + private boolean inCongestionRecovery(Deadline sentTime) { + return (congestionRecoveryStartTime != null && + !sentTime.isAfter(congestionRecoveryStartTime)); + } + + private void onCongestionEvent(Deadline sentTime) { + if (inCongestionRecovery(sentTime)) { + return; + } + congestionRecoveryStartTime = timeSource.instant(); + ssThresh = congestionWindow / 2; + congestionWindow = Math.max(minimumWindow, ssThresh); + maxBytesInFlight = 0; + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + + private static boolean inFlight(QuicPacket packet) { + // packet is in flight if it contains anything other than a single ACK frame + // specifically, a packet containing padding is considered to be in flight. + return packet.frames().size() != 1 || + !(packet.frames().get(0) instanceof AckFrame); + } + + @Override + public boolean canSendPacket() { + lock.lock(); + try { + if (bytesInFlight >= MAX_BYTES_IN_FLIGHT) { + return false; + } + var canSend = congestionWindow - bytesInFlight >= maxDatagramSize; + return canSend; + } finally { + lock.unlock(); + } + } + + @Override + public void updateMaxDatagramSize(int newSize) { + lock.lock(); + try { + if (minimumWindow != newSize * 2) { + minimumWindow = newSize * 2; + maxDatagramSize = newSize; + congestionWindow = Math.max(congestionWindow, minimumWindow); + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetSent(int packetBytes) { + lock.lock(); + try { + bytesInFlight += packetBytes; + if (bytesInFlight > maxBytesInFlight) { + maxBytesInFlight = bytesInFlight; + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetAcked(int packetBytes, Deadline sentTime) { + lock.lock(); + try { + bytesInFlight -= packetBytes; + // RFC 9002 says we should not increase cwnd when application limited. + // The concept itself is poorly defined. + // Here we limit cwnd growth based on the maximum bytes in flight + // observed since the last congestion event + if (inCongestionRecovery(sentTime)) { + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Acked, in recovery: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } + return; + } + boolean isAppLimited; + if (congestionWindow < ssThresh) { + isAppLimited = congestionWindow >= 2 * maxBytesInFlight; + if (!isAppLimited) { + congestionWindow += packetBytes; + } + } else { + isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize; + if (!isAppLimited) { + congestionWindow += Math.max((long) maxDatagramSize * packetBytes / congestionWindow, 1L); + } + } + if (Log.quicCC()) { + if (isAppLimited) { + Log.logQuic(dbgTag+ " Acked, not blocked: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } else { + Log.logQuic(dbgTag + " Acked, increased: bytes: " + packetBytes + + ", in flight: " + bytesInFlight + + ", new cwnd:" + congestionWindow); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { + lock.lock(); + try { + for (QuicPacket packet : lostPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + onCongestionEvent(sentTime); + if (persistent) { + congestionWindow = minimumWindow; + congestionRecoveryStartTime = null; + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Persistent congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetDiscarded(Collection discardedPackets) { + lock.lock(); + try { + for (QuicPacket packet : discardedPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java new file mode 100644 index 00000000000..0e2f9401bc0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Utils; + +/** + * Estimator for quic connection round trip time. + * Defined in + * RFC 9002 section 5. + * Takes RTT samples as input (max 1 sample per ACK frame) + * Produces: + * - minimum RTT over a period of time (minRtt) for internal use + * - exponentially weighted moving average (smoothedRtt) + * - mean deviation / variation in the observed samples (rttVar) + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +public class QuicRttEstimator { + + // The property indicates the maximum number of retries. The constant holds the value + // 2^N where N is the value of the property (clamped between 2 and 20, default 8): + // 1=>2 2=>4 3=>8 4=>16 5=>32 6=>64 7=>128 8=>256 ... etc + public static final long MAX_PTO_BACKOFF = 1L << Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.maxPtoBackoff", 8), + 2, 20); + // The timeout calculated for PTO will stay clamped at MAX_PTO_BACKOFF_TIMEOUT if + // the calculated value exceeds MAX_PTO_BACKOFF_TIMEOUT + public static final Duration MAX_PTO_BACKOFF_TIMEOUT = Duration.ofSeconds(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.maxPtoBackoffTime", 240), + 1, 1200)); + // backoff will continue to be increased past MAX_PTO_BACKOFF if the timeout calculated + // for PTO is less than MIN_PTO_BACKOFF_TIMEOUT + public static final Duration MIN_PTO_BACKOFF_TIMEOUT = Duration.ofSeconds(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.minPtoBackoffTime", 15), + 0, 1200)); + + private static final long INITIAL_RTT = TimeUnit.MILLISECONDS.toMicros(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.initialRTT", 333), + 50, 1000)); + + // kGranularity, 1ms is recommended by RFC 9002 section 6.1.2 + private static final long GRANULARITY_MICROS = TimeUnit.MILLISECONDS.toMicros(1); + private Deadline firstSample; + private long latestRttMicros; + private long minRttMicros; + private long smoothedRttMicros = INITIAL_RTT; + private long rttVarMicros = INITIAL_RTT / 2; + private long ptoBackoffFactor = 1; + private long rttSampleCount = 0; + + public record QuicRttEstimatorState(long latestRttMicros, + long minRttMicros, + long smoothedRttMicros, + long rttVarMicros, + long rttSampleCount) {} + + public synchronized QuicRttEstimatorState state() { + return new QuicRttEstimatorState(latestRttMicros, minRttMicros, smoothedRttMicros, rttVarMicros, rttSampleCount); + } + + /** + * Update the estimator with latest RTT sample. + * Use only samples where: + * - the largest acknowledged PN is newly acknowledged + * - at least one of the newly acked packets is ack-eliciting + * @param latestRttMicros time between when packet was sent + * and ack was received, in microseconds + * @param ackDelayMicros ack delay received in ack frame, decoded to microseconds + * @param now time at which latestRttMicros was calculated + */ + public synchronized void consumeRttSample(long latestRttMicros, long ackDelayMicros, Deadline now) { + this.rttSampleCount += 1; + this.latestRttMicros = latestRttMicros; + if (firstSample == null) { + firstSample = now; + minRttMicros = latestRttMicros; + smoothedRttMicros = latestRttMicros; + rttVarMicros = latestRttMicros / 2; + } else { + minRttMicros = Math.min(minRttMicros, latestRttMicros); + long adjustedRtt; + if (latestRttMicros >= minRttMicros + ackDelayMicros) { + adjustedRtt = latestRttMicros - ackDelayMicros; + } else { + adjustedRtt = latestRttMicros; + } + rttVarMicros = (3 * rttVarMicros + Math.abs(smoothedRttMicros - adjustedRtt)) / 4; + smoothedRttMicros = (7 * smoothedRttMicros + adjustedRtt) / 8; + } + } + + /** + * {@return time threshold for time-based loss detection} + * See + * RFC 9002 section 6.1.2 + * + */ + public synchronized Duration getLossThreshold() { + // max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity) + long maxRttMicros = Math.max(smoothedRttMicros, latestRttMicros); + long lossThresholdMicros = Math.max(9*maxRttMicros / 8, GRANULARITY_MICROS); + return Duration.of(lossThresholdMicros, ChronoUnit.MICROS); + } + + /** + * {@return the amount of time to wait for acknowledgement of a sent packet, + * excluding max ack delay} + * See + * RFC 9002 section 6.1.2 + */ + public synchronized Duration getBasePtoDuration() { + // PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay + // max_ack_delay is applied by the caller + long basePtoMicros = smoothedRttMicros + + Math.max(4 * rttVarMicros, GRANULARITY_MICROS); + return Duration.of(basePtoMicros, ChronoUnit.MICROS); + } + + public synchronized boolean isMinBackoffTimeoutExceeded() { + return MIN_PTO_BACKOFF_TIMEOUT.compareTo(getBasePtoDuration().multipliedBy(ptoBackoffFactor)) < 0; + } + + public synchronized long getPtoBackoff() { + return ptoBackoffFactor; + } + + public synchronized long increasePtoBackoff() { + // limit to make sure we don't accidentally overflow + if (ptoBackoffFactor <= MAX_PTO_BACKOFF || !isMinBackoffTimeoutExceeded()) { + ptoBackoffFactor *= 2; // can go up to 2 * MAX_PTO_BACKOFF + } + return ptoBackoffFactor; + } + + public synchronized void resetPtoBackoff() { + ptoBackoffFactor = 1; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java new file mode 100644 index 00000000000..2bce3415b17 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint.QuicVirtualThreadedEndpoint; +import jdk.internal.net.http.quic.QuicEndpoint.QuicSelectableEndpoint; + + +/** + * A QUIC selector to select over one or several quic transport + * endpoints. + */ +public abstract sealed class QuicSelector implements Runnable, AutoCloseable + permits QuicSelector.QuicNioSelector, QuicSelector.QuicVirtualThreadPoller { + + /** + * The maximum timeout passed to Selector::select. + */ + public static final long IDLE_PERIOD_MS = 1500; + + private static final TimeLine source = TimeSource.source(); + final Logger debug = Utils.getDebugLogger(this::name); + + private final String name; + private volatile boolean done; + private final QuicInstance instance; + private final QuicSelectorThread thread; + private final QuicTimerQueue timerQueue; + + private QuicSelector(QuicInstance instance, String name) { + this.instance = instance; + this.name = name; + this.timerQueue = new QuicTimerQueue(this::wakeup, debug); + this.thread = new QuicSelectorThread(this); + } + + public String name() { + return name; + } + + // must be overridden by subclasses + public void register(T endpoint) throws ClosedChannelException { + if (debug.on()) debug.log("attaching %s", endpoint); + } + + // must be overridden by subclasses + public void wakeup() { + if (debug.on()) debug.log("waking up selector"); + } + + public QuicTimerQueue timer() { + return timerQueue; + } + + /** + * A {@link QuicSelector} implementation based on blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * Virtual Threads to poll the channels. + * This implementation is tied to {@link QuicVirtualThreadedEndpoint} instances. + */ + static final class QuicVirtualThreadPoller extends QuicSelector { + + static final boolean usePlatformThreads = + Utils.getBooleanProperty("jdk.internal.httpclient.quic.poller.usePlatformThreads", false); + + static final class EndpointTask implements Runnable { + + final QuicVirtualThreadedEndpoint endpoint; + final ConcurrentLinkedQueue endpoints; + EndpointTask(QuicVirtualThreadedEndpoint endpoint, + ConcurrentLinkedQueue endpoints) { + this.endpoint = endpoint; + this.endpoints = endpoints; + } + + public void run() { + try { + endpoint.channelReadLoop(); + } finally { + endpoints.remove(this); + } + } + } + + private final Object waiter = new Object(); + private final ConcurrentLinkedQueue endpoints = new ConcurrentLinkedQueue<>(); + private final ReentrantLock stateLock = new ReentrantLock(); + private final ExecutorService virtualThreadService; + + private volatile long wakeups; + + + private QuicVirtualThreadPoller(QuicInstance instance, String name) { + super(instance, name); + virtualThreadService = usePlatformThreads + ? Executors.newThreadPerTaskExecutor(Thread.ofPlatform() + .name(name + "-pt-worker", 1).factory()) + : Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name(name + "-vt-worker-", 1).factory()); + if (debug.on()) debug.log("created"); + } + + ExecutorService readLoopExecutor() { + return virtualThreadService; + } + + @Override + public void register(QuicVirtualThreadedEndpoint endpoint) throws ClosedChannelException { + super.register(endpoint); + endpoint.attach(this); + } + + public Future startReading(QuicVirtualThreadedEndpoint endpoint) { + EndpointTask task; + stateLock.lock(); + try { + if (done()) throw new ClosedSelectorException(); + task = new EndpointTask(endpoint, endpoints); + endpoints.add(task); + return virtualThreadService.submit(task); + } finally { + stateLock.unlock(); + } + } + + void markDone() { + // use stateLock to prevent startReading + // to be called *after* shutdown. + stateLock.lock(); + try { + super.shutdown(); + } finally { + stateLock.unlock(); + } + } + + @Override + public void shutdown() { + markDone(); + try { + virtualThreadService.shutdown(); + } finally { + wakeup(); + } + } + + @Override + public void wakeup() { + super.wakeup(); + synchronized (waiter) { + wakeups++; + // there's only one thread that can be waiting + // on waiter - the thread that executes the run() + // method. + waiter.notify(); + } + } + + @Override + public void run() { + try { + if (debug.on()) debug.log("started"); + long waited = 0; + while (!done()) { + var wakeups = this.wakeups; + long timeout = Math.min(computeNextDeadLine(), IDLE_PERIOD_MS); + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: wait(%s) wakeups:%s (+%s), waited:%s", + name(), timeout, this.wakeups, this.wakeups - wakeups, waited)); + } else if (debug.on()) { + debug.log("wait(%s) wakeups:%s (+%s), waited: %s", + timeout, this.wakeups, this.wakeups - wakeups, waited); + } + long wwaited = -1; + synchronized (waiter) { + if (done()) return; + if (wakeups == this.wakeups) { + var start = System.nanoTime(); + waiter.wait(timeout); + var stop = System.nanoTime(); + wwaited = waited = (stop - start) / 1000_000; + } else waited = 0; + } + if (wwaited != -1 && wwaited < timeout) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: waked up early: waited %s, timeout %s", + name(), waited, timeout)); + } + } + } + } catch (Throwable t) { + if (done()) return; + if (debug.on()) debug.log("Selector failed", t); + if (Log.errors()) { + Log.logError("QuicVirtualThreadPoller: selector exiting due to " + t); + Log.logError(t); + } + abort(t); + } finally { + if (debug.on()) debug.log("exiting"); + if (!done()) markDone(); + timer().stop(); + endpoints.removeIf(this::close); + virtualThreadService.close(); + } + } + + boolean close(EndpointTask task) { + try { + task.endpoint.close(); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint %s: %s", task.endpoint.name(), e); + } + } + return true; + } + + boolean abort(EndpointTask task, Throwable error) { + try { + task.endpoint.abort(error); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint %s: %s", task.endpoint.name(), e); + } + } + return true; + } + + @Override + public void abort(Throwable t) { + super.shutdown(); + endpoints.removeIf(task -> abort(task, t)); + super.abort(t); + } + } + + /** + * A {@link QuicSelector} implementation based on non-blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * NIO {@link Selector}. + * This implementation is tied to {@link QuicSelectableEndpoint} instances. + */ + static final class QuicNioSelector extends QuicSelector { + final Selector selector; + + private QuicNioSelector(QuicInstance instance, Selector selector, String name) { + super(instance, name); + this.selector = selector; + if (debug.on()) debug.log("created"); + } + + + public void register(QuicSelectableEndpoint endpoint) throws ClosedChannelException { + super.register(endpoint); + endpoint.attach(selector); + selector.wakeup(); + } + + public void wakeup() { + super.wakeup(); + selector.wakeup(); + } + + /** + * Shuts down the {@code QuicSelector} by marking the + * {@linkplain QuicSelector#shutdown() selector done}, + * and {@linkplain Selector#wakeup() waking up the + * selector thread}. + * Upon waking up, the selector thread will invoke + * {@link Selector#close()}. + * This method doesn't wait for the selector thread to terminate. + * @see #awaitTermination(long, TimeUnit) + */ + public void shutdown() { + super.shutdown(); + selector.wakeup(); + } + + @Override + public void run() { + try { + if (debug.on()) debug.log("started"); + while (!done()) { + long timeout = Math.min(computeNextDeadLine(), IDLE_PERIOD_MS); + // selected = 0 indicates that no key had its ready ops changed: + // it doesn't mean that no key is ready. Therefore - if a key + // was ready to read, and is again ready to read, it doesn't + // increment the selected count. + if (debug.on()) debug.log("select(%s)", timeout); + int selected = selector.select(timeout); + var selectedKeys = selector.selectedKeys(); + if (debug.on()) { + debug.log("Selected: changes=%s, keys=%s", selected, selectedKeys.size()); + } + + // We do not synchronize on selectedKeys: selectedKeys is only + // modified in this thread, whether directly, by calling selectedKeys.clear() below, + // or indirectly, by calling selector.close() below. + for (var key : selectedKeys) { + QuicSelectableEndpoint endpoint = (QuicSelectableEndpoint) key.attachment(); + if (debug.on()) { + debug.log("selected(%s): %s", Utils.readyOps(key), endpoint); + } + try { + endpoint.selected(key.readyOps()); + } catch (CancelledKeyException x) { + if (debug.on()) { + debug.log("Key for %s cancelled: %s", endpoint.name(), x); + } + } + } + // need to clear the selected keys. select won't do that. + selectedKeys.clear(); + } + } catch (Throwable t) { + if (done()) return; + if (debug.on()) debug.log("Selector failed", t); + if (Log.errors()) { + Log.logError("QuicNioSelector: selector exiting due to " + t); + Log.logError(t); + } + abort(t); + } finally { + if (debug.on()) debug.log("exiting"); + timer().stop(); + + try { + selector.close(); + } catch (IOException io) { + if (debug.on()) debug.log("failed to close selector: " + io); + } + } + } + + boolean abort(SelectionKey key, Throwable error) { + try { + QuicSelectableEndpoint endpoint = (QuicSelectableEndpoint) key.attachment(); + endpoint.abort(error); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint associated with key %s: %s", key, error); + } + } + return true; + } + + @Override + public void abort(Throwable error) { + super.shutdown(); + try { + if (selector.isOpen()) { + for (var k : selector.keys()) { + abort(k, error); + } + } + } catch (ClosedSelectorException cse) { + // ignore + } finally { + super.abort(error); + } + } + } + + public long computeNextDeadLine() { + Deadline now = source.instant(); + Deadline deadline = timerQueue.processEventsAndReturnNextDeadline(now, instance.executor()); + if (deadline.equals(Deadline.MAX)) return IDLE_PERIOD_MS; + if (deadline.equals(Deadline.MIN)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: %s millis until %s", name, 1, "now")); + } + return 1; + } + now = source.instant(); + long millis = now.until(deadline, ChronoUnit.MILLIS); + // millis could be 0 if the next deadline is within 1ms of now. + // in that case, round up millis to 1ms since returning 0 + // means the selector would block indefinitely + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: %s millis until %s", + name, (millis <= 0L ? 1L : millis), deadline)); + } + return millis <= 0L ? 1L : millis; + } + + public void start() { + thread.start(); + } + + /** + * Shuts down the {@code QuicSelector} by invoking {@link Selector#close()}. + * This method doesn't wait for the selector thread to terminate. + * @see #awaitTermination(long, TimeUnit) + */ + public void shutdown() { + if (debug.on()) debug.log("closing"); + done = true; + } + + boolean done() { + return done; + } + + /** + * Awaits termination of the selector thread, up until + * the given timeout has elapsed. + * If the current thread is the selector thread, returns + * immediately without waiting. + * + * @param timeout the maximum time to wait for termination + * @param unit the timeout unit + */ + public void awaitTermination(long timeout, TimeUnit unit) { + if (Thread.currentThread() == thread) { + return; + } + try { + thread.join(unit.toMillis(timeout)); + } catch (InterruptedException ie) { + if (debug.on()) debug.log("awaitTermination interrupted: " + ie); + Thread.currentThread().interrupt(); + } + } + + /** + * Closes this {@code QuicSelector}. + * This method calls {@link #shutdown()} and then {@linkplain + * #awaitTermination(long, TimeUnit) waits for the selector thread + * to terminate}, up to two {@link #IDLE_PERIOD_MS}. + */ + @Override + public void close() { + shutdown(); + awaitTermination(IDLE_PERIOD_MS * 2, TimeUnit.MILLISECONDS); + } + + @Override + public String toString() { + return name; + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + shutdown(); + } + + static class QuicSelectorThread extends Thread { + QuicSelectorThread(QuicSelector selector) { + super(null, selector, + "Thread(%s)".formatted(selector.name()), + 0, false); + this.setDaemon(true); + } + } + + /** + * {@return a new instance of {@code QuicNioSelector}} + *

    + * A {@code QuicNioSelector} is an implementation of {@link QuicSelector} + * based on non blocking {@linkplain DatagramChannel Datagram Channels} and + * using an underlying {@linkplain Selector NIO Selector}. + *

    + * The returned implementation can only be used with + * {@link QuicSelectableEndpoint} endpoints. + * + * @param quicInstance the quic instance + * @param name the selector name + * @throws IOException if an IOException occurs when creating the underlying {@link Selector} + */ + public static QuicSelector createQuicNioSelector(QuicInstance quicInstance, String name) + throws IOException { + Selector selector = Selector.open(); + return new QuicNioSelector(quicInstance, selector, name); + } + + /** + * {@return a new instance of {@code QuicVirtualThreadPoller}} + * A {@code QuicVirtualThreadPoller} is an implementation of + * {@link QuicSelector} based on blocking {@linkplain DatagramChannel + * Datagram Channels} and using {@linkplain Thread#ofVirtual() + * Virtual Threads} to poll the datagram channels. + *

    + * The returned implementation can only be used with + * {@link QuicVirtualThreadedEndpoint} endpoints. + * + * @param quicInstance the quic instance + * @param name the selector name + */ + public static QuicSelector createQuicVirtualThreadPoller(QuicInstance quicInstance, String name) { + return new QuicVirtualThreadPoller(quicInstance, name); + } +} diff --git a/src/hotspot/share/runtime/reflectionUtils.cpp b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java similarity index 60% rename from src/hotspot/share/runtime/reflectionUtils.cpp rename to src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java index ba1e167f1d6..e5802fef20c 100644 --- a/src/hotspot/share/runtime/reflectionUtils.cpp +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,20 +21,18 @@ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. - * */ +package jdk.internal.net.http.quic; -#include "classfile/javaClasses.hpp" -#include "classfile/vmClasses.hpp" -#include "oops/instanceKlass.inline.hpp" -#include "runtime/reflectionUtils.hpp" +/** + * Used internally to indicate Quic stream limit has been reached + */ +public final class QuicStreamLimitException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 4181770819022847041L; -GrowableArray *FilteredFieldsMap::_filtered_fields = - new (mtServiceability) GrowableArray(3, mtServiceability); - - -void FilteredFieldsMap::initialize() { - int offset = reflect_ConstantPool::oop_offset(); - _filtered_fields->append(new FilteredField(vmClasses::reflect_ConstantPool_klass(), offset)); + public QuicStreamLimitException(String message) { + super(message); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java new file mode 100644 index 00000000000..9269b12bf64 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Comparator; + +import jdk.internal.net.http.common.Deadline; + +/** + * Models an event which is triggered upon reaching a + * deadline. {@code QuicTimedEvent} instances are designed to be + * registered with a single {@link QuicTimerQueue}. + * + * @implSpec + * Implementations should make sure that each instance of + * {@code QuicTimedEvent} is only present once in a single + * {@link QuicTimerQueue} at any given time. It is however + * allowed to register the event again with the same {@link QuicTimerQueue} + * after it has been handled, or if it is no longer registered in any + * queue. + */ +sealed interface QuicTimedEvent + permits PacketSpaceManager.PacketTransmissionTask, + QuicTimerQueue.Marker, + QuicEndpoint.ClosedConnection, + IdleTimeoutManager.IdleTimeoutEvent, + IdleTimeoutManager.StreamDataBlockedEvent, + QuicConnectionImpl.MaxInitialTimer { + + /** + * {@return the deadline at which the event should be triggered, + * or {@link Deadline#MAX} if the event does not need + * to be scheduled} + * @implSpec + * Care should be taken to not change the deadline while the + * event is registered with a {@link QuicTimerQueue timer queue}. + * The only safe time when the deadline can be changed is: + *

      + *
    • when {@link #refreshDeadline()} method, since the event + * is not in any queue at that point,
    • + *
    • when the deadline is {@link Deadline#MAX}, since the + * event should not be in any queue if it has no + * deadline
    • + *
    + * + */ + Deadline deadline(); + + /** + * Handles the triggered event. + * Returns a new deadline, if the event needs to be + * rescheduled, or {@code Deadline.MAX} otherwise. + * + * @implSpec + * The {@link #deadline() deadline} should not be + * changed before {@link #refreshDeadline()} is called. + * + * @return a new deadline if the event should be + * rescheduled right away, {@code Deadline.MAX} + * otherwise. + */ + Deadline handle(); + + /** + * An event id, obtained at construction time from + * {@link QuicTimerQueue#newEventId()}. This is used + * to implement a total order among subclasses. + * @return this event's id. + */ + long eventId(); + + /** + * {@return true if this event's deadline is before the + * other's event deadline} + * + * @implSpec + * The default implementation of this method is to return {@code + * deadline().isBefore(other.deadline())}. + * + * @param other the other event + */ + default boolean isBefore(QuicTimedEvent other) { + return deadline().isBefore(other.deadline()); + } + + /** + * Compares this event's deadline with the other event's deadline. + * + * @implSpec + * The default implementation of this method compares deadlines in the same manner as + * {@link Deadline#compareTo(Deadline) this.deadline().compareTo(other.deadline())} would. + * + * @param other the other event + * + * @return {@code -1}, {@code 0}, or {@code 1} depending on whether this + * event's deadline is before, equals to, or after, the other event's + * deadline. + */ + default int compareDeadlines(QuicTimedEvent other) { return deadline().compareTo(other.deadline());} + + /** + * Called to cause an event to refresh its deadline. + * This method is called by the {@link QuicTimerQueue} + * when rescheduling an event. + * @apiNote + * The value returned by {@link #deadline()} can only be safely + * updated when this method is called. + */ + Deadline refreshDeadline(); + + /** + * Compares two instance of {@link QuicTimedEvent}. + * First compared their {@link #deadline()}, then their {@link #eventId()}. + * It is expected that two elements with same deadline and same event id + * must the same {@link QuicTimedEvent} instance. + * + * @param one a first QuicTimedEvent instance + * @param two a second QuicTimedEvent instance + * @return whether the first element is less, same, or greater than the + * second. + */ + static int compare(QuicTimedEvent one, QuicTimedEvent two) { + if (one == two) return 0; + int cmp = one.compareDeadlines(two); + cmp = cmp == 0 ? Long.compare(one.eventId(), two.eventId()) : cmp; + // ensure total ordering; + assert cmp != 0 || one.equals(two) && two.equals(one); + return cmp; + } + + /** + * A comparator that compares {@code QuicTimedEvent} instances by their deadline, in the same + * manner as {@link #compare(QuicTimedEvent, QuicTimedEvent) QuicTimedEvent::compare}. + */ + // public static final (are redundant) + Comparator COMPARATOR = QuicTimedEvent::compare; + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java new file mode 100644 index 00000000000..830415593cb --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; + +/** + * A timer queue that can process events which are due, and possibly + * reschedule them if needed. An instance of a {@link QuicTimerQueue} + * is usually associated with an instance of {@link QuicSelector} which + * provides the timer/wakeup facility. + */ +public final class QuicTimerQueue { + + // A queue that contains scheduled events + private final ConcurrentSkipListSet scheduled = + new ConcurrentSkipListSet<>(QuicTimedEvent.COMPARATOR); + + // A queue that contains events which are due. The queue is + // filled by processAndReturnNextDeadline() + private final ConcurrentLinkedQueue due = + new ConcurrentLinkedQueue<>(); + + // A queue that contains events that need to be rescheduled. + // The event may already be in the scheduled queue - in which + // case it will be removed before being added back. + private final Set rescheduled = + ConcurrentHashMap.newKeySet(); + + // A callback to tell the timer thread to wake up + private final Runnable notifier; + + // A loop to process events which are due, or which need to + // be rescheduled. + private final SequentialScheduler processor = + SequentialScheduler.lockingScheduler(this::processDue); + + private final Logger debug; + private volatile boolean closed; + private volatile Deadline scheduledDeadline = Deadline.MAX; + private volatile Deadline returnedDeadline = Deadline.MAX; + + /** + * Creates a new timer queue with the given notifier. + * A notifier is used to notify the timer thread that + * new events have been added to the queue of scheduled + * event. The notifier should wake up the thread and + * trigger a call to either {@link + * #processEventsAndReturnNextDeadline(Deadline, Executor)} + * or {@link #nextDeadline()}. + * + * @param notifier A notifier to wake up the timer thread when + * new event have been added and the next + * deadline has changed. + */ + public QuicTimerQueue(Runnable notifier, Logger debug) { + this.notifier = notifier; + this.debug = debug; + } + + // For debug purposes only + private String d(Deadline deadline) { + return Utils.debugDeadline(debugNow(), deadline); + } + + // For debug purposes only + private String d(Deadline now, Deadline deadline) { + return Utils.debugDeadline(now, deadline); + } + + // For debug purposes only + private Deadline debugNow() { + return TimeSource.now(); + } + + /** + * Schedule the given event by adding it to the timer queue. + * + * @param event an event to be scheduled + */ + public void offer(QuicTimedEvent event) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + Deadline deadline = event.deadline(); + scheduled.add(event); + scheduled(deadline); + if (debug.on()) debug.log("QuicTimerQueue: event %s offered", event); + if (notify(deadline)) { + if (debug.on()) debug.log("QuicTimerQueue: event %s will be rescheduled", event); + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be scheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + notifier.run(); + } else { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will not be scheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + } + } + + /** + * The next deadline for this timer queue. This is only weakly + * consistent. If the queue is empty, {@link Deadline#MAX} is + * returned. + * + * @return The next deadline, or {@code Deadline.MAX}. + */ + public Deadline nextDeadline() { + var event = scheduled.ceiling(FLOOR); + return event == null ? Deadline.MAX : event.deadline(); + } + + public Deadline pendingScheduledDeadline() { + return scheduledDeadline; + } + + /** + * Process all events that were due before {@code now}, and + * returns the next deadline. The events are processed within + * an executor's thread, so this method may return before all + * events have been processed. The events are processed in + * order, with respect to their deadline. Processing an event + * involves invoking its {@link QuicTimedEvent#handle() handle} + * method. If that method returns a new deadline different from + * {@link Deadline#MAX} the processed event is rescheduled + * immediately. Otherwise, it will not be rescheduled. + * + * @param now The point in time before which events are + * considered to be due. Usually, that's now. + * @param executor An executor to process events which are due. + * + * @return the next unexpired deadline, or {@link Deadline#MAX} + * if the queue is empty. + */ + public Deadline processEventsAndReturnNextDeadline(Deadline now, Executor executor) { + QuicTimedEvent event; + int drained = 0; + int dues; + synchronized (this) { + scheduledDeadline = Deadline.MAX; + } + // moved scheduled / rescheduled tasks to due, until + // nothing else is due. Then process dues. + do { + dues = processRescheduled(now); + dues = dues + processScheduled(now); + drained += dues; + } while (dues > 0); + Deadline newDeadline = (event = scheduled.ceiling(FLOOR)) == null ? Deadline.MAX : event.deadline(); + assert event == null || newDeadline.isBefore(Deadline.MAX) : "Invalid deadline for " + event; + if (debug.on()) { + debug.log("QuicTimerQueue: newDeadline: " + d(now, newDeadline) + + (event == null ? "no event scheduled" : (" for " + event))); + } + Deadline next; + synchronized (this) { + var scheduled = scheduledDeadline; + scheduledDeadline = Deadline.MAX; + // if some task is being rescheduled with a deadline + // that is before any scheduled deadline, use that deadline. + next = returnedDeadline = min(newDeadline, scheduled); + } + if (next.equals(Deadline.MAX)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: no deadline" + + " (scheduled: %s, rescheduled: %s, dues %s)", + Thread.currentThread().getName(), this.scheduled.size(), + this.rescheduled.size(), this.due.size())); + } + } + if (drained > 0) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: %s events to handle (%s in dues)", + Thread.currentThread().getName(), drained, this.due.size())); + } + processor.runOrSchedule(executor); + } + return next; + } + + // return the deadline which is before the other + private Deadline min(Deadline one, Deadline two) { + return one.isBefore(two) ? one : two; + } + + // walk through the rescheduled tasks and moves any + // that are due to `due`. Otherwise, move them to + // `scheduled` + private int processRescheduled(Deadline now) { + int drained = 0; + for (var it = rescheduled.iterator(); it.hasNext(); ) { + QuicTimedEvent event = it.next(); + it.remove(); // remove before processing to avoid race + scheduled.remove(event); + Deadline deadline = event.refreshDeadline(); + if (deadline.equals(Deadline.MAX)) { + continue; + } + if (deadline.isAfter(now)) { + scheduled.add(event); + } else { + due.add(event); + drained++; + } + } + if (drained > 0) { + if (debug.on()) { + debug.log("QuicTimerQueue: %s rescheduled tasks are due", drained); + } + } + return drained; + } + + // walk through the scheduled tasks and moves any + // that are due to `due`. + private int processScheduled(Deadline now) { + QuicTimedEvent event; + int drained = 0; + while ((event = scheduled.ceiling(FLOOR)) != null) { + Deadline deadline = event.deadline(); + if (!isDue(deadline, now)) { + break; + } + event = scheduled.pollFirst(); + if (event == null) { + break; + } + drained++; + due.add(event); + } + if (drained > 0 && debug.on()) { + debug.log("QuicTimerQueue: %s scheduled tasks are due", drained); + } + return drained; + } + + private static boolean isDue(final Deadline deadline, final Deadline now) { + return deadline.compareTo(now) <= 0; + } + + // process all due events in order + private void processDue() { + try { + QuicTimedEvent event; + if (closed) return; + if (debug.on()) debug.log("QuicTimerQueue: processDue"); + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: process %s events", + Thread.currentThread().getName(), due.size())); + } + Deadline minDeadLine = Deadline.MAX; + while ((event = due.poll()) != null) { + if (closed) return; + Deadline nextDeadline = event.handle(); + if (Deadline.MAX.equals(nextDeadline)) continue; + rescheduled.add(event); + if (nextDeadline.isBefore(minDeadLine)) minDeadLine = nextDeadline; + } + + // record the minimal deadline that was rescheduled + scheduled(minDeadLine); + + // wake up the selector thread if necessary + if (notify(minDeadLine)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: notify: minDeadline: %s", + Thread.currentThread().getName(), d(minDeadLine))); + } + notifier.run(); + } else if (!minDeadLine.equals(Deadline.MAX)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: no need to notify: minDeadline: %s", + Thread.currentThread().getName(), d(minDeadLine))); + } + } + + } catch (Throwable t) { + if (!closed) { + if (Log.errors()) { + Log.logError(Thread.currentThread().getName() + + ": Unexpected exception while processing due events: " + t); + Log.logError(t); + } else if (debug.on()) { + debug.log("Unexpected exception while processing due events", t); + } + throw t; + } else { + if (Log.errors()) { + Log.logError(Thread.currentThread().getName() + + ": Ignoring exception while closing: " + t); + Log.logError(t); + } else if (debug.on()) { + debug.log("Ignoring exception while closing: " + t); + } + } + } + } + + // We do not need to notify the selector thread if the next scheduled + // deadline is before the given deadline, or if it is after + // the last returned deadline. + private boolean notify(Deadline deadline) { + synchronized (this) { + if (deadline.isBefore(nextDeadline()) + || deadline.isBefore(returnedDeadline)) { + return true; + } + } + return false; + } + + // Record a prospective attempt to reschedule an event at + // the given deadline + private Deadline scheduled(Deadline deadline) { + synchronized (this) { + var scheduled = scheduledDeadline; + if (deadline.isBefore(scheduled)) { + scheduledDeadline = deadline; + return deadline; + } + return scheduled; + } + } + + /** + * Reschedule the given {@code QuicTimedEvent}. + * + * @apiNote + * This method is used if the prospective future deadline at which the event + * should be scheduled is not known by the caller. + * This may cause an idle wakeup in the selector thread owning this + * {@code QuicTimerQueue}. Use {@link #reschedule(QuicTimedEvent, Deadline)} + * to minimize idle wakeup. + * + * @param event an event to reschedule + */ + public void reschedule(QuicTimedEvent event) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + rescheduled.add(event); + if (debug.on()) debug.log("QuicTimerQueue: event %s will be rescheduled", event); + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be rescheduled" + + " (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, returnedDeadline), + d(now, nextDeadline()))); + } + notifier.run(); + } + + /** + * Reschedule the given {@code QuicTimedEvent}. + * + * @apiNote + * This method should be used in preference of {@link #reschedule(QuicTimedEvent)} + * if the prospective future deadline at which the event should be scheduled is + * already known by the caller. Using this method will minimize idle wakeup + * of the selector thread, in comparison of {@link #reschedule(QuicTimedEvent)}. + * + * @param event an event to reschedule + * @param deadline the prospective future deadline at which the event should + * be rescheduled + */ + public void reschedule(QuicTimedEvent event, Deadline deadline) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + rescheduled.add(event); + scheduled(deadline); + // no need to wake up the selector thread if the next deadline + // is already before the new deadline + + if (notify(deadline)) { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be rescheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } else if (debug.on()) { + debug.log("QuicTimerQueue: event %s will be rescheduled", event); + } + notifier.run(); + } else { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will not be rescheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + } + } + + private static final AtomicLong EVENTIDS = new AtomicLong(); + + /** + * {@return a unique id for a new {@link QuicTimedEvent}} + * Each new instance of {@link QuicTimedEvent} is created with a long + * ID returned by this method to ensure a total ordering of + * {@code QuicTimedEvent} instances, even when their deadlines + * are equal. + */ + public static long newEventId() { + return EVENTIDS.getAndIncrement(); + } + + // aliases + private static final Marker FLOOR = Marker.FLOOR; + private static final Marker CEILING = Marker.CEILING; + + /** + * Called to clean up the timer queue when it is no longer needed. + * Makes sure that all pending tasks are cleared from the various lists. + */ + public void stop() { + closed = true; + do { + processor.stop(); + due.clear(); + rescheduled.clear(); + scheduled.clear(); + } while (!due.isEmpty() || !rescheduled.isEmpty() || !scheduled.isEmpty()); + } + + // This class is used to work around the lack of a peek() method + // in ConcurrentSkipListSet. ConcurrentSkipListSet has a method + // called first(), but it throws NoSuchElementException if the + // set isEmpty() - whereas peek() would return {@code null}. + // The next best thing is to use ConcurrentSkipListSet::ceiling, + // but for that we need to define a minimum event which is lower + // than any other event: we do this by defining Marker.FLOOR + // which has deadline=Deadline.MIN and eventId=Long.MIN_VALUE; + // Note: it would be easier to use a record, but an enum ensures that we + // can only have the two instances FLOOR and CEILING. + enum Marker implements QuicTimedEvent { + /** + * A {@code Marker} event to pass to {@link ConcurrentSkipListSet#ceiling(Object) + * ConcurrentSkipListSet::ceiling} in order to get the first event in the list, + * or {@code null}. + * + * @apiNote + * The intended usage is:
    {@code
    +         *       var head = scheduled.ceiling(FLOOR);
    +         * }
    + * + */ + FLOOR(Deadline.MIN, Long.MIN_VALUE), + /** + * A {@code Marker} event to pass to {@link ConcurrentSkipListSet#floor(Object) + * ConcurrentSkipListSet::floor} in order to get the last event in the list, + * or {@code null}. + * + * @apiNote + * The intended usage is:
    {@code
    +         *       var head = scheduled.floor(CEILING);
    +         * }
    + * + */ + CEILING(Deadline.MAX, Long.MAX_VALUE); + private final Deadline deadline; + private final long eventId; + private Marker(Deadline deadline, long eventId) { + this.deadline = deadline; + this.eventId = eventId; + } + + @Override public Deadline deadline() { return deadline; } + @Override public Deadline refreshDeadline() {return Deadline.MAX;} + @Override public Deadline handle() { return Deadline.MAX; } + @Override public long eventId() { return eventId; } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java new file mode 100644 index 00000000000..36832575add --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java @@ -0,0 +1,1319 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.EnumMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.quic.QuicVersion; + +/** + * This class models a collection of Quic transport parameters. This class is mutable + * and not thread safe. + * + * A parameter is considered absent if {@link #getParameter(TransportParameterId)} + * yields {@code null}. The parameter is present otherwise. + * Parameters can be removed by calling {@link + * #setParameter(TransportParameterId, byte[]) setParameter(id, null)}. + * The methods {@link #getBooleanParameter(TransportParameterId)} and + * {@link #getIntParameter(TransportParameterId)} allow easy access to + * parameters whose type is boolean or int, respectively. + * When such a parameter is absent, its default value is returned by + * those methods. + + * From + * RFC 9000, section 18.2: + * + *
    + *
    {@code
    + * Many transport parameters listed here have integer values.
    + * Those transport parameters that are identified as integers use a
    + * variable-length integer encoding; see Section 16. Transport parameters
    + * have a default value of 0 if the transport parameter is absent, unless
    + * otherwise stated.
    + * }
    + * + *

    [...] + * + *

    {@code
    + * If present, transport parameters that set initial per-stream flow control limits
    + * (initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, and
    + * initial_max_stream_data_uni) are equivalent to sending a MAX_STREAM_DATA frame
    + * (Section 19.10) on every stream of the corresponding type immediately after opening.
    + * If the transport parameter is absent, streams of that type start with a flow control
    + * limit of 0.
    + *
    + * A client MUST NOT include any server-only transport parameter:
    + *        original_destination_connection_id,
    + *        preferred_address,
    + *        retry_source_connection_id, or
    + *        stateless_reset_token.
    + *
    + * A server MUST treat receipt of any of these transport parameters as a connection error
    + * of type TRANSPORT_PARAMETER_ERROR.
    + * }
    + *
    + * + * @see ParameterId + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class QuicTransportParameters { + + /** + * An interface to model a transport parameter ID. + * A transport parameter ID has a {@linkplain #name() name} (which is + * not transmitted) and an {@linkplain #idx() identifier}. + * Standard parameters are modeled by enum values in + * {@link ParameterId}. + */ + public sealed interface TransportParameterId { + /** + * {@return the transport parameter name} + * This a human-readable string. + */ + String name(); + + /** + * {@return the transport parameter identifier} + */ + int idx(); + + /** + * {@return the parameter id corresponding to the given identifier, if + * defined, an empty optional otherwise} + * @param idx a parameter identifier + */ + static Optional valueOf(long idx) { + return ParameterId.valueOf(idx); + } + } + + /** + * Standard Quic transport parameter names and ids. + * These are the transport parameters defined in IANA + * "QUIC Transport Parameters" registry. + * @see + * RFC 9000, Section 22.3 + */ + public enum ParameterId implements TransportParameterId { + /** + * original_destination_connection_id (0x00). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     This parameter is the value of the Destination Connection ID field
    +         *     from the first Initial packet sent by the client; see Section 7.3.
    +         *     This transport parameter is only sent by a server.
    +         * }
    + * @see RFC 9000, Section 7.3 + */ + original_destination_connection_id(0x00), + + /** + * max_idle_timeout (0x01). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The maximum idle timeout is a value in milliseconds that is encoded
    +         *     as an integer; see (Section 10.1).
    +         *     Idle timeout is disabled when both endpoints omit this transport
    +         *     parameter or specify a value of 0.
    +         * }
    + * @see RFC 9000, Section 10.1 + */ + max_idle_timeout(0x01), + + /** + * stateless_reset_token (0x02). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     A stateless reset token is used in verifying a stateless reset;
    +         *     see Section 10.3.
    +         *     This parameter is a sequence of 16 bytes. This transport parameter MUST NOT
    +         *     be sent by a client but MAY be sent by a server. A server that does not send
    +         *     this transport parameter cannot use stateless reset (Section 10.3) for
    +         *     the connection ID negotiated during the handshake.
    +         * }
    + * @see RFC 9000, Section 10.3 + */ + stateless_reset_token(0x02), + + /** + * max_udp_payload_size (0x03). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The maximum UDP payload size parameter is an integer value that limits the
    +         *     size of UDP payloads that the endpoint is willing to receive. UDP datagrams
    +         *     with payloads larger than this limit are not likely to be processed by
    +         *     the receiver.
    +         *
    +         *     The default for this parameter is the maximum permitted UDP payload of 65527.
    +         *     Values below 1200 are invalid.
    +         *
    +         *     This limit does act as an additional constraint on datagram size
    +         *     in the same way as the path MTU, but it is a property of the endpoint
    +         *     and not the path; see Section 14.
    +         *     It is expected that this is the space an endpoint dedicates to
    +         *     holding incoming packets.
    +         * }
    + * @see RFC 9000, Section 14 + */ + max_udp_payload_size(0x03), + + /** + * initial_max_data (0x04). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The initial maximum data parameter is an integer value that contains
    +         *     the initial value for the maximum amount of data that can be sent on
    +         *     the connection. This is equivalent to sending a MAX_DATA (Section 19.9)
    +         *     for the connection immediately after completing the handshake.
    +         * }
    + * @see RFC 9000, Section 19.9 + */ + initial_max_data(0x04), + + /** + * initial_max_stream_data_bidi_local (0x05). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     This parameter is an integer value specifying the initial flow control
    +         *     limit for locally initiated bidirectional streams. This limit applies to
    +         *     newly created bidirectional streams opened by the endpoint that
    +         *     sends the transport parameter.
    +         *     In client transport parameters, this applies to streams with an identifier
    +         *     with the least significant two bits set to 0x00;
    +         *     in server transport parameters, this applies to streams with the least
    +         *     significant two bits set to 0x01.
    +         * }
    + */ + initial_max_stream_data_bidi_local(0x05), + + /** + * initial_max_stream_data_bidi_remote (0x06). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     This parameter is an integer value specifying the initial flow control
    +         *     limit for peer-initiated bidirectional streams. This limit applies to
    +         *     newly created bidirectional streams opened by the endpoint that receives
    +         *     the transport parameter. In client transport parameters, this applies to
    +         *     streams with an identifier with the least significant two bits set to 0x01;
    +         *     in server transport parameters, this applies to streams with the least
    +         *     significant two bits set to 0x00.
    +         * }
    + */ + initial_max_stream_data_bidi_remote(0x06), + + /** + * initial_max_stream_data_uni (0x07). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     This parameter is an integer value specifying the initial flow control
    +         *     limit for unidirectional streams. This limit applies to newly created
    +         *     unidirectional streams opened by the endpoint that receives the transport
    +         *     parameter. In client transport parameters, this applies to streams with
    +         *     an identifier with the least significant two bits set to 0x03; in server
    +         *     transport parameters, this applies to streams with the least significant
    +         *     two bits set to 0x02.
    +         * }
    + */ + initial_max_stream_data_uni(0x07), + + /** + * initial_max_streams_bidi (0x08). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The initial maximum bidirectional streams parameter is an integer value
    +         *     that contains the initial maximum number of bidirectional streams the
    +         *     endpoint that receives this transport parameter is permitted to initiate.
    +         *     If this parameter is absent or zero, the peer cannot open bidirectional
    +         *     streams until a MAX_STREAMS frame is sent. Setting this parameter is equivalent
    +         *     to sending a MAX_STREAMS (Section 19.11) of the corresponding type with the
    +         *     same value.
    +         * }
    + * @see RFC 9000, Section 19.11 + */ + initial_max_streams_bidi(0x08), + + /** + * initial_max_streams_uni (0x09). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The initial maximum unidirectional streams parameter is an integer value that
    +         *     contains the initial maximum number of unidirectional streams the endpoint
    +         *     that receives this transport parameter is permitted to initiate. If this parameter
    +         *     is absent or zero, the peer cannot open unidirectional streams until a MAX_STREAMS
    +         *     frame is sent. Setting this parameter is equivalent to sending a MAX_STREAMS
    +         *     (Section 19.11) of the corresponding type with the same value.
    +         * }
    + * @see RFC 9000, Section 19.11 + */ + initial_max_streams_uni(0x09), + + /** + * ack_delay_exponent (0x0a). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The acknowledgment delay exponent is an integer value indicating an exponent
    +         *     used to decode the ACK Delay field in the ACK frame (Section 19.3). If this
    +         *     value is absent, a default value of 3 is assumed (indicating a multiplier of 8).
    +         *     Values above 20 are invalid.
    +         * }
    + * @see RFC 9000, Section 19.3 + */ + ack_delay_exponent(0x0a), + + /** + * max_ack_delay (0x0b). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The maximum acknowledgment delay is an integer value indicating the maximum
    +         *     amount of time in milliseconds by which the endpoint will delay sending acknowledgments.
    +         *     This value SHOULD include the receiver's expected delays in alarms firing. For example,
    +         *     if a receiver sets a timer for 5ms and alarms commonly fire up to 1ms late, then it
    +         *     should send a max_ack_delay of 6ms. If this value is absent, a default of 25
    +         *     milliseconds is assumed. Values of 2^14 or greater are invalid.
    +         * }
    + */ + max_ack_delay(0x0b), + + /** + * disable_active_migration (0x0c). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The disable active migration transport parameter is included if the endpoint does not
    +         *     support active connection migration (Section 9) on the address being used during the
    +         *     handshake. An endpoint that receives this transport parameter MUST NOT use a new local
    +         *     address when sending to the address that the peer used during the handshake. This transport
    +         *     parameter does not prohibit connection migration after a client has acted on a
    +         *     preferred_address transport parameter. This parameter is a zero-length value.
    +         * }
    + * @see RFC 9000, Section 9 + */ + disable_active_migration(0x0c), + + /** + * preferred_address (0x0d). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     The server's preferred address is used to effect a change in server address at the
    +         *     end of the handshake, as described in Section 9.6. This transport parameter is only
    +         *     sent by a server.
    +         *     Servers MAY choose to only send a preferred address of one address family
    +         *     by sending an all-zero address and port (0.0.0.0:0 or [::]:0) for the
    +         *     other family. IP addresses are encoded in network byte order.
    +         *
    +         *     The preferred_address transport parameter contains an address and port for both
    +         *     IPv4 and IPv6. The four-byte IPv4 Address field is followed by the associated
    +         *     two-byte IPv4 Port field. This is followed by a 16-byte IPv6 Address field and
    +         *     two-byte IPv6 Port field. After address and port pairs, a Connection ID Length
    +         *     field describes the length of the following Connection ID field.
    +         *     Finally, a 16-byte Stateless Reset Token field includes the stateless reset
    +         *     token associated with the connection ID. The format of this transport parameter
    +         *     is shown in Figure 22 below.
    +         *
    +         *     The Connection ID field and the Stateless Reset Token field contain an alternative
    +         *     connection ID that has a sequence number of 1; see Section 5.1.1. Having these values
    +         *     sent alongside the preferred address ensures that there will be at least one
    +         *     unused active connection ID when the client initiates migration to the preferred
    +         *     address.
    +         *
    +         *     The Connection ID and Stateless Reset Token fields of a preferred address are
    +         *     identical in syntax and semantics to the corresponding fields of a NEW_CONNECTION_ID
    +         *     frame (Section 19.15). A server that chooses a zero-length connection ID MUST NOT
    +         *     provide a preferred address. Similarly, a server MUST NOT include a zero-length
    +         *     connection ID in this transport parameter. A client MUST treat a violation of
    +         *     these requirements as a connection error of type TRANSPORT_PARAMETER_ERROR.
    +         *
    +         * Preferred Address {
    +         *   IPv4 Address (32),
    +         *   IPv4 Port (16),
    +         *   IPv6 Address (128),
    +         *   IPv6 Port (16),
    +         *   Connection ID Length (8),
    +         *   Connection ID (..),
    +         *   Stateless Reset Token (128),
    +         * }
    +         *
    +         * Figure 22: Preferred Address Format
    +         * }
    + * @see RFC 9000, Section 5.1.1 + * @see RFC 9000, Section 9.6 + * @see RFC 9000, Section 19.15 + */ + preferred_address(0x0d), + + /** + * active_connection_id_limit (0x0e). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     This is an integer value specifying the maximum number of connection IDs from
    +         *     the peer that an endpoint is willing to store. This value includes the connection
    +         *     ID received during the handshake, that received in the preferred_address transport
    +         *     parameter, and those received in NEW_CONNECTION_ID frames. The value of the
    +         *     active_connection_id_limit parameter MUST be at least 2. An endpoint that receives
    +         *     a value less than 2 MUST close the connection with an error of type
    +         *     TRANSPORT_PARAMETER_ERROR. If this transport parameter is absent, a default of 2 is
    +         *     assumed. If an endpoint issues a zero-length connection ID, it will never send a
    +         *     NEW_CONNECTION_ID frame and therefore ignores the active_connection_id_limit value
    +         *     received from its peer.
    +         * }
    + */ + active_connection_id_limit(0x0e), + + /** + * initial_source_connection_id (0x0f). + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +         *     This is the value that the endpoint included in the Source Connection ID field of
    +         *     the first Initial packet it sends for the connection; see Section 7.3.
    +         * }
    + * @see RFC 9000, Section 7.3 + */ + initial_source_connection_id(0x0f), + + /** + * retry_source_connection_id (0x10). + *

    + * From + * RFC 9000, Section 18.2 + *

    {@code
    +         *     This is the value that the server included in the Source Connection ID field of a
    +         *     Retry packet; see Section 7.3. This transport parameter is only sent by a server.
    +         * }
    + * @see RFC 9000, Section 7.3 + */ + retry_source_connection_id(0x10), + + /** + * version_information (0x11). + *

    + * From + * RFC 9368, Section 3 + *

    {@code
    +         *     During the handshake, endpoints will exchange Version Information,
    +         *     which consists of a Chosen Version and a list of Available Versions.
    +         *     Any version of QUIC that supports this mechanism MUST provide a mechanism
    +         *     to exchange Version Information in both directions during the handshake,
    +         *     such that this data is authenticated.
    +         * }
    + */ + version_information(0x11); + + /* + * Reserved Transport Parameters (31 * N + 27 for int values of N) + *

    + * From + * RFC 9000, Section 18.1 + *

    {@code
    +         *     Transport parameters with an identifier of the form 31 * N + 27
    +         *     for integer values of N are reserved to exercise the requirement
    +         *     that unknown transport parameters be ignored. These transport
    +         *     parameters have no semantics and can carry arbitrary values.
    +         * }
    + */ + // No values are defined here, but these will be + // ignored if received (see + // sun.security.ssl.QuicTransportParametersExtension). + + /** + * The number of known transport parameters. + * This is also the number of enum values defined by the + * {@link ParameterId} enumeration. + */ + private static final int PARAMETERS_COUNT = ParameterId.values().length; + + ParameterId(int idx) { + // idx() and valueOf() assume that idx = ordinal; + // if that's no longer the case, update the implementation + // and remove this assert. + assert idx == ordinal(); + } + + @Override + public int idx() { + return ordinal(); + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + + private static Optional valueOf(long idx) { + if (idx < 0 || idx >= PARAMETERS_COUNT) return Optional.empty(); + return Optional.of(values()[(int)idx]); + } + } + + public record VersionInformation(int chosenVersion, int[] availableVersions) { } + + /** + * A map to store transport parameter values. + * Contains a byte array corresponding to the encoded value + * of the parameter. + */ + private final Map values; + + /** + * Constructs a new empty array of Quic transport parameters. + */ + public QuicTransportParameters() { + values = new EnumMap<>(ParameterId.class); + } + + /** + * Constructs a new collection of Quic transport parameters initialized + * from the specified collection. + * @param params the parameter collection used to initialize this object * + */ + public QuicTransportParameters(QuicTransportParameters params) { + values = new EnumMap<>(params.values); + } + + /** + * {@return true if the given parameter is present, false otherwise} + * @apiNote + * This is equivalent to {@link #getParameter(TransportParameterId) + * getParameter(id) != null}, but avoids cloning the parameter value. + * @param id the parameter id + */ + public boolean isPresent(TransportParameterId id) { + byte[] value = values.get((ParameterId) id); + return value != null; + } + + /** + * {@return the value of the given parameter, as a byte array, or + * {@code null} if the parameter is absent. + * @param id the parameter id + */ + public byte[] getParameter(TransportParameterId id) { + byte[] value = values.get((ParameterId) id); + return value == null ? null : value.clone(); + } + + /** + * {@return true if the value of the given parameter matches the given connection ID} + * @param id the transport parameter id + * @param connectionId the connection id to match against + */ + public boolean matches(TransportParameterId id, QuicConnectionId connectionId) { + byte[] value = values.get((ParameterId) id); + return connectionId.matches(ByteBuffer.wrap(value).asReadOnlyBuffer()); + } + + /** + * Sets the value of the given parameter. + * If the given value is {@code null}, the parameter is removed. + * @param id the parameter id + * @param value the new parameter value, or {@code null}. + * @throws IllegalArgumentException if the given value is invalid for + * the given parameter id + */ + public void setParameter(TransportParameterId id, byte[] value) { + ParameterId pid = checkParameterValue(id, value); + if (value != null) { + values.put(pid, value.clone()); + } else { + values.remove(pid); + } + } + + /** + * {@return the value of the given parameter, as an unsigned int + * in the range {@code [0, 2^62 - 1]}} + * If the parameter is not present its default value (as specified in the RFC) is returned. + * @param id the parameter id + * @throws IllegalArgumentException if the value of the given parameter + * cannot be decoded as a variable length unsigned int + */ + public long getIntParameter(TransportParameterId id) { + return getIntParameter((ParameterId)id); + } + + private long getIntParameter(final ParameterId pid) { + return switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] value = values.get(pid); + final long res; + if (value == null) { + res = switch (pid) { + case active_connection_id_limit -> 2; + case max_udp_payload_size -> 65527; + case ack_delay_exponent -> 3; + case max_ack_delay -> 25; + default -> 0; + }; + } else { + res = decodeVLIntFully(pid, ByteBuffer.wrap(value)); + } + yield res; + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + + }; + } + + /** + * {@return the value of the given parameter, as an unsigned int + * in the range {@code [0, 2^62 - 1]}} + * If the parameter is not present then {@code defaultValue} is returned. + * @param id the parameter id + * @throws IllegalArgumentException if the value of the given parameter + * cannot be decoded as a variable length unsigned int or if the {@code defaultValue} + * exceeds the maximum allowed value for variable length integer + */ + public long getIntParameter(TransportParameterId id, long defaultValue) { + if (defaultValue > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("default value " + defaultValue + + " exceeds maximum allowed variable length" + + " integer value " + VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + ParameterId pid = (ParameterId)id; + return switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] value = values.get(pid); + final long res; + if (value == null) { + res = defaultValue; + } else { + res = decodeVLIntFully(pid, ByteBuffer.wrap(value)); + } + yield res; + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + }; + } + + /** + * Sets the value of the given parameter, as an unsigned int. + * If a negative value is provided, the parameter is removed. + * + * @param id the parameter id + * @param value the new value of the parameter, or a negative value + * + * @throws IllegalArgumentException if the value of the given parameter is + * not an int, or if the provided value is out of range + */ + public void setIntParameter(TransportParameterId id, long value) { + ParameterId pid = (ParameterId)id; + switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] v = null; + if (value >= 0) { + int length = VariableLengthEncoder.getEncodedSize(value); + if (length <= 0) throw new IllegalArgumentException("failed to encode " + value); + int size = VariableLengthEncoder.encode(ByteBuffer.wrap(v = new byte[length]), value); + assert size == length; + checkParameterValue(pid, v); + } + setOrRemove(pid, v); + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + } + } + + /** + * {@return the value of the given parameter, as a boolean} + * If the parameter is not present its default value (false) + * is returned. + * + * @param id the parameter id + * + * @throws IllegalArgumentException if the value of the given parameter + * is not a boolean + */ + public boolean getBooleanParameter(TransportParameterId id) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.disable_active_migration) { + throw new IllegalArgumentException(String.valueOf(id)); + } + return values.get(pid) != null; + } + + /** + * Sets the value of the given parameter, as a boolean. + * @apiNote + * It is not possible to distinguish between a boolean parameter + * whose value is absent and a parameter whose value is false. + * Both are represented by a {@code null} value in the parameter + * array. + * @param id the parameter id + * @param value the new value of the parameter + * @throws IllegalArgumentException if the value of the given parameter is + * not a boolean + */ + public void setBooleanParameter(TransportParameterId id, boolean value) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.disable_active_migration) { + throw new IllegalArgumentException(String.valueOf(id)); + } + setOrRemove(pid, value ? NOBYTES : null); + } + + private void setOrRemove(ParameterId pid, byte[] value) { + if (value != null) { + values.put(pid, value); + } else { + values.remove(pid); + } + } + + /** + * {@return the value of the given parameter, as {@link VersionInformation}} + * If the parameter is not present {@code null} is returned + * + * @param id the parameter id + * + * @throws IllegalArgumentException if the value of the given parameter + * is not a version information + * @throws QuicTransportException if the parameter value has incorrect length, + * or if any version is equal to zero + */ + public VersionInformation getVersionInformationParameter(TransportParameterId id) + throws QuicTransportException { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.version_information) { + throw new IllegalArgumentException(String.valueOf(id)); + } + byte[] val = values.get(pid); + if (val == null) { + return null; + } + if (val.length < 4 || (val.length & 3) != 0) { + throw new QuicTransportException( + "Invalid version information length " + val.length, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + ByteBuffer bbval = ByteBuffer.wrap(val); + assert bbval.order() == ByteOrder.BIG_ENDIAN; + int chosen = bbval.getInt(); + if (chosen == 0) { + throw new QuicTransportException( + "[version_information] Chosen Version = 0", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + int[] available = new int[bbval.remaining() / 4]; + for (int i = 0; i < available.length; i++) { + int version = bbval.getInt(); + if (version == 0) { + throw new QuicTransportException( + "[version_information] Available Version = 0", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + available[i] = version; + } + return new VersionInformation(chosen, available); + } + + /** + * Sets the value of the given parameter, as {@link VersionInformation}. + * @param id the parameter id + * @param value the new value of the parameter + * @throws IllegalArgumentException if the value of the given parameter is + * not a version information + */ + public void setVersionInformationParameter(TransportParameterId id, VersionInformation value) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.version_information) { + throw new IllegalArgumentException(String.valueOf(id)); + } + byte[] val = new byte[value.availableVersions.length * 4 + 4]; + ByteBuffer bbval = ByteBuffer.wrap(val); + assert bbval.order() == ByteOrder.BIG_ENDIAN; + bbval.putInt(value.chosenVersion); + for (int available : value.availableVersions) { + bbval.putInt(available); + } + assert !bbval.hasRemaining(); + values.put(pid, val); + } + + /** + * {@return a {@link VersionInformation} object corresponding to the specified versions} + * @param chosenVersion chosen version + * @param availableVersions available versions + */ + public static VersionInformation buildVersionInformation( + QuicVersion chosenVersion, List availableVersions) { + int[] available = new int[availableVersions.size()]; + for (int i = 0; i < available.length; i++) { + available[i] = availableVersions.get(i).versionNumber(); + } + return new VersionInformation(chosenVersion.versionNumber(), available); + } + + /** + * Sets the value of a parameter whose format corresponds to the + * {@link ParameterId#preferred_address} parameter. + * @param id the parameter id + * @param ipv4 the preferred IPv4 address (or the IPv4 wildcard address) + * @param port4 the preferred IPv4 port (or 0) + * @param ipv6 the preferred IPv6 address (or the IPv6 wildcard address) + * @param port6 the preferred IPv6 port (or 0) + * @param connectionId the connection id bytes + * @param statelessToken the stateless token + * @throws IllegalArgumentException if any of the given parameters has an + * illegal value, or if the given parameter value is not of the + * {@link ParameterId#preferred_address} format + * @see ParameterId#preferred_address + */ + public void setPreferredAddressParameter(TransportParameterId id, + Inet4Address ipv4, int port4, + Inet6Address ipv6, int port6, + ByteBuffer connectionId, + ByteBuffer statelessToken) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.preferred_address) { + throw new IllegalArgumentException(String.valueOf(id)); + } + int cidlen = connectionId.remaining(); + if (cidlen == 0 || cidlen > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException( + "connection id len out of range [1..20]: " + cidlen); + } + int tklen = statelessToken.remaining(); + if (tklen != TOKEN_SIZE) { + throw new IllegalArgumentException("bad stateless token length: expected 16, found " + tklen); + } + if (port4 < 0 || port4 > MAX_PORT) + throw new IllegalArgumentException("IPv4 port out of range: " + port4); + if (port6 < 0 || port6 > MAX_PORT) + throw new IllegalArgumentException("IPv6 port out of range: " + port6); + int size = MIN_PREF_ADDR_SIZE + cidlen; + byte[] value = new byte[size]; + ByteBuffer buffer = ByteBuffer.wrap(value); + if (!ipv4.isAnyLocalAddress()) { + buffer.put(IPV4_ADDR_OFFSET, ipv4.getAddress()); + } + buffer.putShort(IPV4_PORT_OFFSET, (short) port4); + if (!ipv6.isAnyLocalAddress()) { + buffer.put(IPV6_ADDR_OFFSET, ipv6.getAddress()); + } + buffer.putShort(IPV6_PORT_OFFSET, (short)port6); + buffer.put(CID_LEN_OFFSET, (byte) cidlen); + buffer.put(CID_OFFSET, connectionId, connectionId.position(), cidlen); + assert size - CID_OFFSET - cidlen == TOKEN_SIZE : (size - CID_OFFSET - cidlen); + assert tklen == TOKEN_SIZE; + buffer.put(CID_OFFSET + cidlen, statelessToken, statelessToken.position(), tklen); + values.put(pid, value); + } + + /** + * {@return the size in bytes required to encode the parameter + * array} + */ + public int size() { + int size = 0; + for (var kv : values.entrySet()) { + var i = kv.getKey().idx(); + var value = kv.getValue(); + if (value == null) continue; + assert value.length > 0 || i == ParameterId.disable_active_migration.idx(); + size += VariableLengthEncoder.getEncodedSize(i); + size += VariableLengthEncoder.getEncodedSize(value.length); + size += value.length; + } + return size; + } + + /** + * Encodes the transport parameters into the given byte buffer. + *

    + * From + * RFC 9000, Section 18.2: + *

    {@code
    +     * The extension_data field of the quic_transport_parameters
    +     * extension defined in [QUIC-TLS] contains the QUIC transport
    +     * parameters. They are encoded as a sequence of transport
    +     * parameters, as shown in Figure 20:
    +     *
    +     * Transport Parameters {
    +     *   Transport Parameter (..) ...,
    +     * }
    +     *
    +     * Figure 20: Sequence of Transport Parameters
    +     *
    +     * Each transport parameter is encoded as an (identifier, length,
    +     * value) tuple, as shown in Figure 21:
    +     *
    +     * Transport Parameter {
    +     *   Transport Parameter ID (i),
    +     *   Transport Parameter Length (i),
    +     *   Transport Parameter Value (..),
    +     * }
    +     * }
    + * + * @param buffer a byte buffer in which to encode the transport parameters + * @return the number of bytes written + * @throws BufferOverflowException if there is not enough space in the + * provided buffer + * @see jdk.internal.net.quic.QuicTLSEngine#setLocalQuicTransportParameters(ByteBuffer) + * @see + * RFC 9000, Section 18 + * @see + * RFC 9001 [QUIC-TLS] + */ + public int encode(ByteBuffer buffer) { + int start = buffer.position(); + for (var kv : values.entrySet()) { + var i = kv.getKey().idx(); + var value = kv.getValue(); + if (value == null) continue; + + VariableLengthEncoder.encode(buffer, i); + VariableLengthEncoder.encode(buffer, value.length); + buffer.put(value); + } + var written = buffer.position() - start; + if (QuicTransportParameters.class.desiredAssertionStatus()) { + int size = size(); + assert written == size + : "unexpected number of bytes encoded: %d, expected %d" + .formatted(written, size); + } + return written; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Quic Transport Params["); + for (var kv : values.entrySet()) { + var param = kv.getKey(); + var value = kv.getValue(); + if (value != null) { + // param is set + // we just return the string representation of the param ids and don't include + // the encoded values + sb.append(param); + sb.append(", "); + } + } + return sb.append("]").toString(); + } + + // values for (variable length) integer params are decoded, for other params + // that are set, the value is printed as a hex string. + public String toStringWithValues() { + final StringBuilder sb = new StringBuilder("Quic Transport Params["); + for (var kv : values.entrySet()) { + var param = kv.getKey(); + var value = kv.getValue(); + if (value != null) { + // param is set, so include it in the string representation + sb.append(param); + final String valAsString = valueToString(param); + sb.append("=").append(valAsString); + sb.append(", "); + } + } + return sb.append("]").toString(); + } + + private String valueToString(final ParameterId parameterId) { + assert this.values.get(parameterId) != null : "param " + parameterId + " not set"; + try { + return switch (parameterId) { + // int params + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, + initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> + String.valueOf(getIntParameter(parameterId)); + default -> + '"' + HexFormat.of().formatHex(values.get(parameterId)) + '"'; + }; + } catch (RuntimeException e) { + // if the value was a malformed integer, return the hex representation + return '"' + HexFormat.of().formatHex(values.get(parameterId)) + '"'; + } + } + + /** + * Decodes the quic transport parameters from the given buffer. + * Parameters which are not supported are silently discarded. + * + * @param buffer a byte buffer containing the transport parameters + * + * @return the decoded transport parameters + * @throws QuicTransportException if the parameters couldn't be decoded + * + * @see jdk.internal.net.quic.QuicTLSEngine#setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer) (ByteBuffer) + * @see jdk.internal.net.quic.QuicTransportParametersConsumer#accept(ByteBuffer) + * @see #encode(ByteBuffer) + * @see + * RFC 9000, Section 18 + */ + public static QuicTransportParameters decode(ByteBuffer buffer) + throws QuicTransportException { + QuicTransportParameters parameters = new QuicTransportParameters(); + while (buffer.hasRemaining()) { + final long id = VariableLengthEncoder.decode(buffer); + final ParameterId pid = TransportParameterId.valueOf(id) + .orElse(null); + final String name = pid == null ? String.valueOf(id) : pid.toString(); + long length = VariableLengthEncoder.decode(buffer); + if (length < 0) { + throw new QuicTransportException( + "Can't decode length for transport parameter " + name, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (length > buffer.remaining()) { + throw new QuicTransportException("Transport parameter truncated", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + byte[] value = new byte[(int) length]; + buffer.get(value); + if (pid == null) { + // RFC-9000, section 7.4.2: An endpoint MUST ignore transport parameters + // that it does not support. + if (Log.quicControl()) { + Log.logQuic("ignoring unsupported transport parameter: " + name); + } + continue; + } + try { + checkParameterValue(pid, value); + } catch (RuntimeException e) { + throw new QuicTransportException(e.getMessage(), + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + var oldValue = parameters.values.putIfAbsent(pid, value); + if (oldValue != null) { + throw new QuicTransportException( + "Duplicate transport parameter " + name, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + return parameters; + } + + /** + * Reads the preferred address encoded in the value + * of a parameter whose format corresponds to the {@link + * ParameterId#preferred_address} parameter. + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * Otherwise, the returned list contains + * at most one IPv4 address and/or one IPv6 address. + * + * @apiNote + * To obtain the list of addresses encoded in the + * {@link ParameterId#preferred_address} parameter, use + * {@link #getPreferredAddress(TransportParameterId, byte[]) + * getPreferredAddress(ParameterId.preferred_address,} + * {@link #getParameter(TransportParameterId) + * parameters.getParameter(ParameterId.preferred_address)}. + * + * @param id the parameter id + * @param value the value of the parameter + * @return a list of {@link InetSocketAddress}, or {@code null} if the + * given value is {@code null}. + * @see ParameterId#preferred_address + */ + public static List getPreferredAddress( + TransportParameterId id, byte[] value) { + if (value == null) return null; + if (value.length < MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": not enough bytes in value; found " + value.length); + } + ByteBuffer buffer = ByteBuffer.wrap(value); + int ipv4port = buffer.getShort(IPV4_PORT_OFFSET) & 0xFFFF; + int ipv6port = buffer.getShort(IPV6_PORT_OFFSET) & 0xFFFF; + + byte[] ipv4 = new byte[IPV4_SIZE]; + buffer.get(IPV4_ADDR_OFFSET, ipv4); + byte[] ipv6 = new byte[IPV6_SIZE]; + buffer.get(IPV6_ADDR_OFFSET, ipv6); + InetSocketAddress ipv4addr = new InetSocketAddress(getByAddress(id, ipv4), ipv4port); + InetSocketAddress ipv6addr = new InetSocketAddress(getByAddress(id, ipv6), ipv6port); + return Stream.of(ipv4addr, ipv6addr) + .filter((isa) -> !isa.getAddress().isAnyLocalAddress()) + .toList(); + } + + /** + * Reads the connection id bytes from the value of a parameter + * whose format corresponds to the {@link ParameterId#preferred_address} + * parameter. + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * + * @param preferredAddressValue the value of {@link ParameterId#preferred_address} param + * @return the connection id bytes + * @see ParameterId#preferred_address + */ + public static ByteBuffer getPreferredConnectionId(final byte[] preferredAddressValue) { + if (preferredAddressValue == null) { + return null; + } + final int length = getPreferredConnectionIdLength(ParameterId.preferred_address, + preferredAddressValue); + return ByteBuffer.wrap(preferredAddressValue, CID_OFFSET, length); + } + + /** + * Reads the stateless token bytes from the value of a parameter + * whose format corresponds to the {@link ParameterId#preferred_address} + * parameter. + * + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * + * @param preferredAddressValue the value of {@link ParameterId#preferred_address} param + * @return the stateless reset token bytes + * @see ParameterId#preferred_address + */ + public static byte[] getPreferredStatelessResetToken(final byte[] preferredAddressValue) { + if (preferredAddressValue == null) { + return null; + } + final int length = getPreferredConnectionIdLength(ParameterId.preferred_address, + preferredAddressValue); + final int offset = CID_OFFSET + length; + final byte[] statelessResetToken = new byte[TOKEN_SIZE]; + System.arraycopy(preferredAddressValue, offset, statelessResetToken, 0, TOKEN_SIZE); + return statelessResetToken; + } + + static final byte[] NOBYTES = new byte[0]; + static final int IPV6_SIZE = 16; + static final int IPV4_SIZE = 4; + static final int PORT_SIZE = 2; + static final int TOKEN_SIZE = 16; + static final int CIDLEN_SIZE = 1; + static final int IPV4_ADDR_OFFSET = 0; + static final int IPV4_PORT_OFFSET = IPV4_ADDR_OFFSET + IPV4_SIZE; + static final int IPV6_ADDR_OFFSET = IPV4_PORT_OFFSET + PORT_SIZE; + static final int IPV6_PORT_OFFSET = IPV6_ADDR_OFFSET + IPV6_SIZE; + static final int CID_LEN_OFFSET = IPV6_PORT_OFFSET + PORT_SIZE; + static final int CID_OFFSET = CID_LEN_OFFSET + CIDLEN_SIZE; + static final int MIN_PREF_ADDR_SIZE = CID_OFFSET + TOKEN_SIZE; + static final int MAX_PORT = 0xFFFF; + + private static int getPreferredConnectionIdLength(TransportParameterId id, byte[] value) { + if (value.length < MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": not enough bytes in value; found " + value.length); + } + int length = value[CID_LEN_OFFSET] & 0xFF; + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH || length == 0) { + throw new IllegalArgumentException(id + + ": invalid preferred connection ID length: " + length); + } + if (length != value.length - MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": invalid preferred address length: " + value.length + + ", expected: " + (MIN_PREF_ADDR_SIZE + length)); + } + return length; + } + + private static InetAddress getByAddress(TransportParameterId id, byte[] address) { + try { + return InetAddress.getByAddress(address); + } catch (UnknownHostException x) { + // should not happen + throw new IllegalArgumentException(id + + "Invalid address: " + HexFormat.of().formatHex(address)); + } + } + + /** + * verifies that the {@code value} is acceptable (as specified in the RFC) for the + * {@code tpid} + * + * @param tpid the transport parameter id + * @param value the value + * @return the corresponding parameter id if the value is acceptable, else throws a + * {@link IllegalArgumentException} + */ + private static ParameterId checkParameterValue(TransportParameterId tpid, byte[] value) { + ParameterId id = (ParameterId)tpid; + if (value != null) { + switch (id) { + case disable_active_migration -> { + if (value.length > 0) + throw new IllegalArgumentException(id + + ": value must be null or 0-length; found " + + value.length + " bytes"); + } + case stateless_reset_token -> { + if (value.length != 16) + throw new IllegalArgumentException(id + + ": value must be null or 16 bytes long; found " + + value.length + " bytes"); + } + case initial_source_connection_id, original_destination_connection_id, + retry_source_connection_id -> { + if (value.length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException(id + + ": value must not exceed " + + QuicConnectionId.MAX_CONNECTION_ID_LENGTH + + "bytes; found " + value.length + " bytes"); + } + } + case preferred_address -> getPreferredConnectionIdLength(id, value); + case version_information -> { + if (value.length < 4 || value.length % 4 != 0) { + throw new IllegalArgumentException(id + + ": value length must be a positive multiple of 4 " + + "bytes; found " + value.length + " bytes"); + } + } + default -> { + long intvalue; + try { + intvalue = decodeVLIntFully(id, ByteBuffer.wrap(value)); + } catch (IllegalArgumentException x) { + throw x; + } catch (Exception x) { + throw new IllegalArgumentException(id + + ": value is not a valid variable length integer", x); + } + if (intvalue < 0) + throw new IllegalArgumentException(id + + ": value is not a valid variable length integer"); + switch (id) { + case max_udp_payload_size -> { + if (intvalue < 1200 || intvalue > 65527) { + throw new IllegalArgumentException(id + + ": value out of range [1200, 65527]; found " + + intvalue); + } + } + case ack_delay_exponent -> { + if (intvalue > 20) { + throw new IllegalArgumentException(id + + ": value out of range [0, 20]; found " + + intvalue); + } + } + case max_ack_delay -> { + if (intvalue >= (1 << 14)) { + throw new IllegalArgumentException(id + + ": value out of range [0, 2^14); found " + + intvalue); + } + } + case active_connection_id_limit -> { + if (intvalue < 2) { + throw new IllegalArgumentException(id + + ": value out of range [2...]; found " + + intvalue); + } + } + case initial_max_streams_bidi, initial_max_streams_uni -> { + if (intvalue >= 1L << 60) { + throw new IllegalArgumentException(id + + ": value out of range [0,2^60); found " + + intvalue); + } + } + } + } + } + } + return id; + } + + private static long decodeVLIntFully(ParameterId id, ByteBuffer buffer) { + long value = VariableLengthEncoder.decode(buffer); + if (value < 0 || value > (1L << 62) - 1) { + throw new IllegalArgumentException(id + + ": failed to decode variable length integer"); + } + if (buffer.hasRemaining()) + throw new IllegalArgumentException(id + + ": extra bytes in provided value at index " + + buffer.position()); + return value; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java new file mode 100644 index 00000000000..9e441cf7873 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.util.Objects; + +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +// TODO: document this +public abstract sealed class TerminationCause { + private String logMsg; + private String peerVisibleReason; + private final long closeCode; + private final Throwable originalCause; + private final IOException reportedCause; + + private TerminationCause(final long closeCode, final Throwable closeCause) { + this.closeCode = closeCode; + this.originalCause = closeCause; + if (closeCause != null) { + this.logMsg = closeCause.toString(); + } + this.reportedCause = toReportedCause(this.originalCause, this.logMsg); + } + + private TerminationCause(final long closeCode, final String loggedAs) { + this.closeCode = closeCode; + this.originalCause = null; + this.logMsg = loggedAs; + this.reportedCause = toReportedCause(this.originalCause, this.logMsg); + } + + public final long getCloseCode() { + return this.closeCode; + } + + public final IOException getCloseCause() { + return this.reportedCause; + } + + public final String getLogMsg() { + return logMsg; + } + + public final TerminationCause loggedAs(final String logMsg) { + this.logMsg = logMsg; + return this; + } + + public final String getPeerVisibleReason() { + return this.peerVisibleReason; + } + + public final TerminationCause peerVisibleReason(final String reasonPhrase) { + this.peerVisibleReason = reasonPhrase; + return this; + } + + public abstract boolean isAppLayer(); + + public static TerminationCause forTransportError(final QuicTransportErrors err) { + return new TransportError(err); + } + + public static TerminationCause forTransportError(long errorCode, String loggedAs, long frameType) { + return new TransportError(errorCode, loggedAs, frameType); + } + + static SilentTermination forSilentTermination(final String loggedAs) { + return new SilentTermination(loggedAs); + } + + public static TerminationCause forException(final Throwable cause) { + Objects.requireNonNull(cause); + if (cause instanceof QuicTransportException qte) { + return new TransportError(qte); + } + return new InternalError(cause); + } + + // allows for higher (application) layer to inform the connection terminator + // that the higher layer had completed a graceful shutdown of the connection + // and the QUIC layer can now do an immediate close of the connection using + // the {@code closeCode} + public static TerminationCause appLayerClose(final long closeCode) { + return new AppLayerClose(closeCode, (Throwable)null); + } + + public static TerminationCause appLayerClose(final long closeCode, String loggedAs) { + return new AppLayerClose(closeCode, loggedAs); + } + + public static TerminationCause appLayerException(final long closeCode, + final Throwable cause) { + return new AppLayerClose(closeCode, cause); + } + + private static IOException toReportedCause(final Throwable original, + final String fallbackExceptionMsg) { + if (original == null) { + return fallbackExceptionMsg == null + ? new IOException("connection terminated") + : new IOException(fallbackExceptionMsg); + } else if (original instanceof QuicTransportException qte) { + return new IOException(qte.getMessage()); + } else if (original instanceof IOException ioe) { + return ioe; + } else { + return new IOException(original); + } + } + + + static final class TransportError extends TerminationCause { + final long frameType; + final QuicTLSEngine.KeySpace keySpace; + + private TransportError(final QuicTransportErrors err) { + super(err.code(), err.name()); + this.frameType = 0; // unknown frame type + this.keySpace = null; + } + + private TransportError(final QuicTransportException exception) { + super(exception.getErrorCode(), exception); + this.frameType = exception.getFrameType(); + this.keySpace = exception.getKeySpace(); + peerVisibleReason(exception.getReason()); + } + + public TransportError(long errorCode, String loggedAs, long frameType) { + super(errorCode, loggedAs); + this.frameType = frameType; + keySpace = null; + } + + @Override + public boolean isAppLayer() { + return false; + } + } + + static final class InternalError extends TerminationCause { + + private InternalError(final Throwable cause) { + super(QuicTransportErrors.INTERNAL_ERROR.code(), cause); + } + + @Override + public boolean isAppLayer() { + return false; + } + } + + static final class AppLayerClose extends TerminationCause { + private AppLayerClose(final long closeCode, String loggedAs) { + super(closeCode, loggedAs); + } + + // TODO: allow optionally to specify "name" of the close code for app layer + // like "H3_GENERAL_PROTOCOL_ERROR" (helpful in logging) + private AppLayerClose(final long closeCode, final Throwable cause) { + super(closeCode, cause); + } + + @Override + public boolean isAppLayer() { + return true; + } + } + + static final class SilentTermination extends TerminationCause { + + private SilentTermination(final String loggedAs) { + // the error code won't play any role, since silent termination + // doesn't cause any packets to be generated or sent to the peer + super(NO_ERROR.code(), loggedAs); + } + + @Override + public boolean isAppLayer() { + return false; // doesn't play a role in context of silent termination + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java new file mode 100644 index 00000000000..91380fcfca4 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; + +/** + * QUIC packets and frames commonly use a variable-length encoding for + * non-negative values. This encoding ensures that smaller values will use less + * in the packet or frame. + * + *

    The QUIC variable-length encoding reserves the two most significant bits + * of the first byte to encode the size of the length value as a base 2 logarithm + * value. The length itself is then encoded on the remaining bits, in network + * byte order. This means that the length values will be encoded on 1, 2, 4, or + * 8 bytes and can encode 6-, 14-, 30-, or 62-bit values + * respectively, or a value within the range of 0 to 4611686018427387903 + * inclusive. + * + * @spec https://www.rfc-editor.org/rfc/rfc9000.html#integer-encoding + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public class VariableLengthEncoder { + + /** + * The maximum number of bytes on which a variable length + * integer can be encoded. + */ + public static final int MAX_INTEGER_LENGTH = 8; + + /** + * The maximum value a variable length integer can + * take. + */ + public static final long MAX_ENCODED_INTEGER = (1L << 62) - 1; + + static { + assert MAX_ENCODED_INTEGER == 4611686018427387903L; + } + + private VariableLengthEncoder() { + throw new InternalError("should not come here"); + } + + /** + * Decode a variable length value from {@code ByteBuffer}. This method assumes that the + * position of {@code buffer} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last byte read. + * + * @param buffer the {@code ByteBuffer} that the length will be decoded from + * + * @return the value. If an error occurs, {@code -1} is returned and + * the buffer position is left unchanged. + */ + public static long decode(ByteBuffer buffer) { + return decode(BuffersReader.single(buffer)); + } + + /** + * Decode a variable length value from {@code BuffersReader}. This method assumes that the + * position of {@code buffers} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last byte. + * + * @param buffers the {@code BuffersReader} that the length will be decoded from + * + * @return the value. If an error occurs, {@code -1} is returned and + * the buffer position is left unchanged. + */ + public static long decode(BuffersReader buffers) { + if (!buffers.hasRemaining()) + return -1; + + long pos = buffers.position(); + int lenByte = buffers.get(pos) & 0xFF; + pos++; + // read size of length from leading two bits + int prefix = lenByte >> 6; + int len = 1 << prefix; + // retrieve remaining bits that constitute the length + long result = lenByte & 0x3F; + long idx = 0, lim = buffers.limit(); + if (lim - pos < len - 1) return -1; + while (idx++ < len - 1) { + assert pos < lim; + result = ((result << Byte.SIZE) + (buffers.get(pos) & 0xFF)); + pos++; + } + // Set position of ByteBuffer to next byte following length + assert pos == buffers.position() + len; + assert pos <= buffers.limit(); + buffers.position(pos); + + assert (result >= 0) && (result < (1L << 62)); + return result; + } + + /** + * Encode (a variable length) value into {@code ByteBuffer}. This method assumes that the + * position of {@code buffer} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last length byte. + * + * @param buffer the {@code ByteBuffer} that the length will be encoded into + * @param value the variable length value + * + * @throws IllegalArgumentException + * if value supplied falls outside of acceptable bounds [0, 2^62-1], + * or if the given buffer doesn't contain enough space to encode the + * value + * + * @return the {@code position} of the buffer + */ + public static int encode(ByteBuffer buffer, long value) throws IllegalArgumentException { + // check for valid parameters + if (value < 0 || value > MAX_ENCODED_INTEGER) + throw new IllegalArgumentException( + "value supplied falls outside of acceptable bounds"); + if (!buffer.hasRemaining()) + throw new IllegalArgumentException( + "buffer does not contain enough bytes to store length"); + + // set length prefix to indicate size of length + int lengthPrefix = getVariableLengthPrefix(value); + assert lengthPrefix >= 0 && lengthPrefix <= 3; + lengthPrefix <<= (Byte.SIZE - 2); + + int lengthSize = getEncodedSize(value); + assert lengthSize > 0; + assert lengthSize <= 8; + + var limit = buffer.limit(); + var pos = buffer.position(); + + // check that it's possible to add length to buffer + if (lengthSize > limit - pos) + throw new IllegalArgumentException("buffer does not contain enough bytes to store length"); + + // create mask to use in isolating byte to transfer to buffer + long mask = 255L << (Byte.SIZE * (lengthSize - 1)); + // convert length to bytes and add to buffer + boolean isFirstByte = true; + for (int i = lengthSize; i > 0; i--) { + assert buffer.hasRemaining() : "no space left at " + (lengthSize - i); + assert mask != 0; + assert mask == (255L << ((i - 1) * 8)) + : "mask: %x, expected %x".formatted(mask, (255L << ((i - 1) * 8))); + + long b = value & mask; + for (int j = i - 1; j > 0; j--) { + b >>= Byte.SIZE; + } + + assert b == (value & mask) >> (8 * (i - 1)); + + if (isFirstByte) { + assert (b & 0xC0) == 0; + buffer.put((byte) (b | lengthPrefix)); + isFirstByte = false; + } else { + buffer.put((byte) b); + } + // move mask over to next byte - avoid carrying sign bit + mask = (mask >>> Byte.SIZE); + } + var bytes = buffer.position() - pos; + assert bytes == lengthSize; + return lengthSize; + } + + /** + * Returns the variable length prefix. + * The variable length prefix is the base 2 logarithm of + * the number of bytes required to encode + * a positive value as a variable length integer: + * [0, 1, 2, 3] for [1, 2, 4, 8] bytes. + * + * @param value the value to encode + * + * @throws IllegalArgumentException + * if the supplied value falls outside the acceptable bounds [0, 2^62-1] + * + * @return the base 2 logarithm of the number of bytes required to encode + * the value as a variable length integer. + */ + public static int getVariableLengthPrefix(long value) throws IllegalArgumentException { + if ((value > MAX_ENCODED_INTEGER) || (value < 0)) + throw new IllegalArgumentException("invalid length"); + + int lengthPrefix; + if (value > (1L << 30) - 1) + lengthPrefix = 3; // 8 bytes + else if (value > (1L << 14) - 1) + lengthPrefix = 2; // 4 bytes + else if (value > (1L << 6) - 1) + lengthPrefix = 1; // 2 bytes + else + lengthPrefix = 0; // 1 byte + + return lengthPrefix; + } + + /** + * Returns the number of bytes needed to encode + * the given value as a variable length integer. + * This a number between 1 and 8. + * + * @param value the value to encode + * + * @return the number of bytes needed to encode + * the given value as a variable length integer. + * + * @throws IllegalArgumentException + * if the value supplied falls outside of acceptable bounds [0, 2^62-1] + */ + public static int getEncodedSize(long value) throws IllegalArgumentException { + if (value < 0 || value > MAX_ENCODED_INTEGER) + throw new IllegalArgumentException("invalid variable length integer: " + value); + return 1 << getVariableLengthPrefix(value); + } + + /** + * Peeks at a variable length value encoded at the given offset. + * If the byte buffer doesn't contain enough bytes to read the + * variable length value, -1 is returned. + * + *

    This method doesn't advance the buffer position. + * + * @param buffer the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the variable length value encoded at the given offset, or -1 + */ + public static long peekEncodedValue(ByteBuffer buffer, int offset) { + return peekEncodedValue(BuffersReader.single(buffer), offset); + } + + /** + * Peeks at a variable length value encoded at the given offset. + * If the byte buffer doesn't contain enough bytes to read the + * variable length value, -1 is returned. + * + * This method doesn't advance the buffer position. + * + * @param buffers the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the variable length value encoded at the given offset, or -1 + */ + public static long peekEncodedValue(BuffersReader buffers, long offset) { + + // figure out on how many bytes the length is encoded. + int size = peekEncodedValueSize(buffers, offset); + if (size <= 0) return -1L; + assert size > 0 && size <= 8; + + // check that we have enough bytes in the buffer + long limit = buffers.limit(); + long pos = offset; + if (limit - size < pos) return -1L; + + // peek at the variable length: + // - read first byte + int first = buffers.get(pos++); + long res = first & 0x3F; + if (size == 1) return res; + + // - read the rest of the bytes + size -= 1; + assert size > 0; + for (int i=0 ; i < size; i++) { + if (limit <= pos) return -1L; + res = (res << 8) | (long) (buffers.get(pos++) & 0xFF); + } + return res; + } + + /** + * Peeks at a variable length value encoded at the given offset, + * and return the number of bytes on which this value is encoded. + * If the byte buffer is empty or the offset is past + * the limit -1 is returned. + * This method doesn't advance the buffer position. + * + * @param buffer the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the number of bytes on which the variable length + * value is encoded at the given offset, or -1 + */ + public static int peekEncodedValueSize(ByteBuffer buffer, int offset) { + return peekEncodedValueSize(BuffersReader.single(buffer), offset); + } + + /** + * Peeks at a variable length value encoded at the given offset, + * and return the number of bytes on which this value is encoded. + * If the byte buffer is empty or the offset is past + * the limit -1 is returned. + * This method doesn't advance the buffer position. + * + * @param buffers the buffers to read from + * @param offset the offset in the buffer to start reading from + * + * @return the number of bytes on which the variable length + * value is encoded at the given offset, or -1 + */ + public static int peekEncodedValueSize(BuffersReader buffers, long offset) { + long limit = buffers.limit(); + long pos = offset; + if (limit <= pos) return -1; + int first = buffers.get(pos); + int prefix = (first & 0xC0) >>> 6; + int size = 1 << prefix; + assert size > 0 && size <= 8; + return size; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java new file mode 100644 index 00000000000..7983d1be4f0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java @@ -0,0 +1,931 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.LongConsumer; +import java.util.stream.LongStream; +import java.util.stream.StreamSupport; + +/** + * An ACK Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class AckFrame extends QuicFrame { + + private final long largestAcknowledged; + private final long ackDelay; + private final int ackRangeCount; + private final List ackRanges; + + private final boolean countsPresent; + private final long ect0Count; + private final long ect1Count; + private final long ecnCECount; + private final int size; + + private static final int COUNTS_PRESENT = 0x1; + + /** + * Reads an {@code AckFrame} from the given buffer. When entering + * this method the buffer position is supposed to be just past + * after the frame type. That, is the frame type has already + * been read. This method moves the position of the buffer to the + * first byte after the read ACK frame. + * @param buffer a buffer containing the ACK frame + * @param type the frame type read from the buffer + * @throws QuicTransportException if the ACK frame was malformed + */ + AckFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(ACK); + int pos = buffer.position(); + largestAcknowledged = decodeVLField(buffer, "largestAcknowledged"); + ackDelay = decodeVLField(buffer, "ackDelay"); + ackRangeCount = decodeVLFieldAsInt(buffer, "ackRangeCount"); + long firstAckRange = decodeVLField(buffer, "firstAckRange"); + long smallestAcknowledged = largestAcknowledged - firstAckRange; + if (smallestAcknowledged < 0) { + throw new QuicTransportException("Negative PN acknowledged", + null, type, + QuicTransportErrors.FRAME_ENCODING_ERROR); + } + var ackRanges = new ArrayList(ackRangeCount + 1); + AckRange first = AckRange.of(0, firstAckRange); + ackRanges.add(0, first); + for (int i=1; i <= ackRangeCount; i++) { + long gap = decodeVLField(buffer, "gap"); + long len = decodeVLField(buffer, "range length"); + ackRanges.add(i, AckRange.of(gap, len)); + smallestAcknowledged -= gap + len + 2; + if (smallestAcknowledged < 0) { + // verify after each range to avoid wrap around + throw new QuicTransportException("Negative PN acknowledged", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + this.ackRanges = List.copyOf(ackRanges); + if (type % 2 == 1) { + // packet contains ECN counts + countsPresent = true; + ect0Count = decodeVLField(buffer, "ect0Count"); + ect1Count = decodeVLField(buffer, "ect1Count"); + ecnCECount = decodeVLField(buffer, "ecnCECount"); + } else { + countsPresent = false; + ect0Count = -1; + ect1Count = -1; + ecnCECount = -1; + } + size = computeSize(); + int wireSize = buffer.position() - pos + getVLFieldLengthFor(getTypeField()); + assert size <= wireSize : "parsed: %s, computed size: %s" + .formatted(wireSize, size); + } + + /** + * Creates the short formed ACK frame with no count totals + */ + public AckFrame(long largestAcknowledged, long ackDelay, List ackRanges) + { + this(largestAcknowledged, ackDelay, ackRanges, -1, -1, -1); + } + + /** + * Creates the long formed ACK frame with count totals + */ + public AckFrame( + long largestAcknowledged, + long ackDelay, + List ackRanges, + long ect0Count, + long ect1Count, + long ecnCECount) + { + super(ACK); + this.largestAcknowledged = requireVLRange(largestAcknowledged, "largestAcknowledged"); + this.ackDelay = requireVLRange(ackDelay, "ackDelay"); + if (ackRanges.size() < 1) { + throw new IllegalArgumentException("insufficient ackRanges"); + } + if (ackRanges.get(0).gap() != 0) { + throw new IllegalArgumentException("first range must have zero gap"); + } + this.ackRanges = List.copyOf(ackRanges); + this.ackRangeCount = ackRanges.size() - 1; + this.countsPresent = ect0Count != -1 || ect1Count != -1 || ecnCECount != -1; + if (countsPresent) { + this.ect0Count = requireVLRange(ect0Count,"ect0Count"); + this.ect1Count = requireVLRange(ect1Count, "ect1Count"); + this.ecnCECount = requireVLRange(ecnCECount, "ecnCECount"); + } else { + this.ect0Count = ect0Count; + this.ect1Count = ect1Count; + this.ecnCECount = ecnCECount; + } + this.size = computeSize(); + } + + @Override + public long getTypeField() { + return ACK | (countsPresent ? COUNTS_PRESENT : 0); + } + + @Override + public boolean isAckEliciting() { return false; } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, largestAcknowledged, "largestAcknowledged"); + encodeVLField(buffer, ackDelay, "ackDelay"); + encodeVLField(buffer, ackRangeCount, "ackRangeCount"); + encodeVLField(buffer, ackRanges.get(0).range(), "firstAckRange"); + for (int i=1; i <= ackRangeCount; i++) { + AckRange ar = ackRanges.get(i); + encodeVLField(buffer, ar.gap(), "gap"); + encodeVLField(buffer, ar.range(), "range"); + } + if (countsPresent) { + // encode the counts + encodeVLField(buffer, ect0Count, "ect0Count"); + encodeVLField(buffer, ect1Count, "ect1Count"); + encodeVLField(buffer, ecnCECount, "ecnCECount"); + } + assert buffer.position() - pos == size(); + } + + private int computeSize() { + int size = getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(largestAcknowledged) + + getVLFieldLengthFor(ackDelay) + + getVLFieldLengthFor(ackRangeCount) + + getVLFieldLengthFor(ackRanges.get(0).range()) + + ackRanges.stream().skip(1).mapToInt(AckRange::size).sum(); + if (countsPresent) { + size = size + getVLFieldLengthFor(ect0Count) + + getVLFieldLengthFor(ect1Count) + + getVLFieldLengthFor(ecnCECount); + } + return size; + } + + @Override + public int size() { return size; } + + /** + * {@return largest packet number acknowledged by this frame} + */ + public long largestAcknowledged() { + return largestAcknowledged; + } + + /** + * The ACK delay + */ + public long ackDelay() { + return ackDelay; + } + + /** + * {@return the number of ack ranges} + * This corresponds to {@link #ackRanges() ackRange.size() -1}. + */ + public long ackRangeCount() { + return ackRangeCount; + } + + /** + * {@return a new {@code AckFrame} identical to this one, but + * with the given {@code ackDelay}}; + * @param ackDelay + */ + public AckFrame withAckDelay(long ackDelay) { + if (ackDelay == this.ackDelay) return this; + return new AckFrame(largestAcknowledged, ackDelay, ackRanges, + ect0Count, ect1Count, ecnCECount); + } + + /** + * An ACK range, composed of a gap and a range. + */ + public record AckRange(long gap, long range) { + public static final AckRange INITIAL = new AckRange(0, 0); + public AckRange { + requireVLRange(gap, "gap"); + requireVLRange(range, "range"); + } + public int size() { + return getVLFieldLengthFor(gap) + getVLFieldLengthFor(range); + } + public static AckRange of(long gap, long range) { + if (gap == 0 && range == 0) return INITIAL; + return new AckRange(gap, range); + } + } + + /** + * The ack ranges. First element is an actual range relative + * to highest acknowledged packet number. Second (if present) + * is a gap and a range following that gap, and so on until the last. + * @return the list of {@code AckRange} where the first ack range + * has a gap of {@code 0} and a range corresponding to + * the {@code First ACK Range}. + */ + public List ackRanges() { + return ackRanges; + } + + /** + * {@return the ECT0 count from this frame or -1 if not present} + */ + public long ect0Count() { + return ect0Count; + } + + /** + * {@return the ECT1 count from this frame or -1 if not present} + */ + public long ect1Count() { + return ect1Count; + } + + /** + * {@return the ECN-CE count from this frame or -1 if not present} + */ + public long ecnCECount() { + return ecnCECount; + } + + /** + * {@return true if this frame contains an acknowledgment for the + * given packet number} + * @param packetNumber a packet number + */ + public boolean isAcknowledging(long packetNumber) { + return isAcknowledging(largestAcknowledged, ackRanges, packetNumber); + } + + /** + * {@return true if the given range is acknowledged by this frame} + * @param first the first packet in the range, inclusive + * @param last the last packet in the range, inclusive + */ + public boolean isRangeAcknowledged(long first, long last) { + return isRangeAcknowledged(largestAcknowledged, ackRanges, first, last); + } + + + /** + * {@return the smallest packet number acknowledged by this {@code AckFrame}} + */ + public long smallestAcknowledged() { + return smallestAcknowledged(largestAcknowledged, ackRanges); + } + + /** + * @return a stream of packet numbers acknowledged by this frame + */ + public LongStream acknowledged() { + return StreamSupport.longStream(new AckFrameSpliterator(this), false); + } + + + private static class AckFrameSpliterator implements Spliterator.OfLong { + + final AckFrame ackFrame; + + AckFrameSpliterator(AckFrame ackFrame) { + this.ackFrame = ackFrame; + this.largest = ackFrame.largestAcknowledged(); + this.smallest = largest + 2; + this.ackRangeIterator = ackFrame.ackRanges.iterator(); + } + + @Override + public long estimateSize() { + // It is costly to compute an estimate, so we just + // return Long.MAX_VALUE instead + return Long.MAX_VALUE; + } + + @Override + public int characteristics() { + // NONNULL - nulls are not expected to be returned by this long spliterator + // IMMUTABLE - ackFrame.ackRanges() returns unmodifiable list, which cannot be + // structurally modified + return NONNULL | IMMUTABLE; + } + + @Override + public OfLong trySplit() { + // null - this spliterator cannot be split + return null; + } + private final Iterator ackRangeIterator; + private volatile long largest; + private volatile long smallest; + private volatile long pn; // the current packet number + + // The stream returns packet number in decreasing order + // (largest packet number is returned first) + private boolean ackAndDecId(LongConsumer action) { + assert ackFrame.isAcknowledging(pn) + : "%s is not acknowledging %s".formatted(ackFrame, pn); + action.accept(pn--); + return true; + } + + @Override + public boolean tryAdvance(LongConsumer action) { + // First call will see pn == 0 and smallest >= 2, + // which guarantees we will not enter the if below + // before pn has been initialized from the + // first ackRange value + if (pn >= smallest) { + return ackAndDecId(action); + } + if (ackRangeIterator.hasNext()) { + var ackRange = ackRangeIterator.next(); + largest = smallest - ackRange.gap() - 2; + smallest = largest - ackRange.range; + pn = largest; + return ackAndDecId(action); + } + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + return o instanceof AckFrame ackFrame + && largestAcknowledged == ackFrame.largestAcknowledged + && ackDelay == ackFrame.ackDelay + && ackRangeCount == ackFrame.ackRangeCount + && countsPresent == ackFrame.countsPresent + && ect0Count == ackFrame.ect0Count + && ect1Count == ackFrame.ect1Count + && ecnCECount == ackFrame.ecnCECount + && ackRanges.equals(ackFrame.ackRanges); + } + + @Override + public int hashCode() { + return Objects.hash(largestAcknowledged, ackDelay, + ackRanges, ect0Count, ect1Count, ecnCECount); + } + + @Override + public String toString() { + String res = "AckFrame(" + + "largestAcknowledged=" + largestAcknowledged + + ", ackDelay=" + ackDelay + + ", ackRanges=[" + prettyRanges() + "]"; + if (countsPresent) res = res + + ", ect0Count=" + ect0Count + + ", ect1Count=" + ect1Count + + ", ecnCECount=" + ecnCECount; + res += ")"; + return res; + } + + private String prettyRanges() { + String result = null; + long largest; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + result = smallest + ".." + largest + (result != null ? ", "+result : ""); + } + return result; + } + + /** + * {@return the largest packet acknowledged by an + * {@link QuicFrame#ACK ACK} frame contained in the + * given packet, or {@code -1L} if the packet + * contains no {@code ACK} frame} + * @param packet a packet that may contain an {@code ACK} frame + */ + public static long largestAcknowledgedInPacket(QuicPacket packet) { + return packet.frames().stream() + .filter(AckFrame.class::isInstance) + .map(AckFrame.class::cast) + .mapToLong(AckFrame::largestAcknowledged) + .max().orElse(-1L); + } + + /** + * A builder that allows to incrementally build the AckFrame + * that will need to be sent, as new packets are received. + * This class is not MT-thread safe. + */ + public static final class AckFrameBuilder { + long largestAckAcked = -1; + long largestAcknowledged = -1; + long ackDelay = 0; + List ackRanges = new ArrayList<>(); + long ect0Count = -1; + long ect1Count = -1; + long ecnCECount = -1; + + /** + * An empty builder. + */ + public AckFrameBuilder() {} + + /** + * A builder initialize from the content of an AckFrame. + * @param frame the {@code AckFrame} to initialize this builder with. + * Must not be {@code null}. + */ + public AckFrameBuilder(AckFrame frame) { + largestAckAcked = -1; + largestAcknowledged = frame.largestAcknowledged; + ackDelay = frame.ackDelay; + ackRanges.addAll(frame.ackRanges); + ect0Count = frame.ect0Count; + ect1Count = frame.ect1Count; + ecnCECount = frame.ecnCECount; + } + + public long getLargestAckAcked() { + return largestAckAcked; + } + + /** + * Drops all acks for packet whose number is smaller + * than the given {@code largestAckAcked}. + * @param largestAckAcked the smallest packet number that + * should be acknowledged by this + * {@link AckFrame}. + * @return this builder + */ + public AckFrameBuilder dropAcksBefore(long largestAckAcked) { + if (largestAckAcked > this.largestAckAcked) { + this.largestAckAcked = largestAckAcked; + return dropIfSmallerThan(largestAckAcked); + } else { + this.largestAckAcked = largestAckAcked; + } + return this; + } + + /** + * Drops all instances of {@link AckRange} after the given + * index in the {@linkplain #ackRanges() Ack Range List}, and compute + * the new smallest packet number now acknowledged by this + * {@link AckFrame}: this computed packet number will then be + * returned by {@link #getLargestAckAcked()}. + * This is a no-op if index is greater or equal to + * {@code ackRanges().size() -1}. + * @param index the index after which ranges should be dropped. + * @return this builder + */ + public AckFrameBuilder dropAckRangesAfter(int index) { + if (index < 0) { + throw new IllegalArgumentException("invalid index %s for size %s" + .formatted(index, ackRanges.size())); + } + if (index >= ackRanges.size() - 1) return this; + long newLargestAckAcked = dropRangesIfAfter(index); + assert newLargestAckAcked > largestAckAcked; + largestAckAcked = newLargestAckAcked; + return this; + } + + /** + * Sets the ack delay. + * @param ackDelay the ack delay. + * @return this builder. + */ + public AckFrameBuilder ackDelay(long ackDelay) { + this.ackDelay = ackDelay; + return this; + } + + /** + * Sets the ect0Count. Passing -1 unsets the ectOcount. + * @param ect0Count the ect0Count + * @return this builder. + */ + public AckFrameBuilder ect0Count(long ect0Count) { + this.ect0Count = ect0Count; + return this; + } + + /** + * Sets the ect1Count. Passing -1 unsets the ect1count. + * @param ect1Count the ect1Count + * @return this builder. + */ + public AckFrameBuilder ect1Count(long ect1Count) { + this.ect1Count = ect1Count; + return this; + } + + /** + * Sets the ecnCECount. Passing -1 unsets the ecnCEOcount. + * @param ecnCECount the ecnCECount + * @return this builder. + */ + public AckFrameBuilder ecnCECount(long ecnCECount) { + this.ecnCECount = ecnCECount; + return this; + } + + /** + * Adds the given packet number to the list of ack ranges. + * If the packet is already being acknowledged by this frame, + * do nothing. + * @param packetNumber the packet number + * @return this builder + */ + public AckFrameBuilder addAck(long packetNumber) { + // check if we need to acknowledge this packet + if (packetNumber <= largestAckAcked) return this; + // System.out.println("adding " + packetNumber); + if (ackRanges.isEmpty()) { + // easy case: we only have one packet to acknowledge! + return acknowledgeFirstPacket(packetNumber); + } else if (packetNumber > largestAcknowledged) { + return acknowledgeLargerPacket(packetNumber); + } else if (packetNumber < largestAcknowledged) { + // now is the complex case: we need to find out: + // - whether this packet is already acknowledged, in which case, + // there is nothing to do (great) + // - or whether we can extend an existing range + // - or whether we need to create a new range (if the packet falls + // within a gap whose value is > 0). + // - or whether we should merge two ranges if the packet falls + // on a gap whose value is 0 + ListIterator iterator = ackRanges.listIterator(); + long largest = largestAcknowledged; + long smallest = largest + 2; + int index = -1; + while (iterator.hasNext()) { + var ackRange = iterator.next(); + // index of the current ackRange element + index++; + // largest packet number acknowledged by this ackRange + largest = smallest - ackRange.gap - 2; + // smallest packet number acknowledged by this ackRange + smallest = largest - ackRange.range; + + // if the packet number we want to acknowledge is greater + // than the largest packet acknowledged by this ackRange + // there are two cases: + if (packetNumber > largest) { + // the packet number is just above the largest packet + if (packetNumber -1 == largest) { + // the current ackRange must have a gap, and we can simply + // reduce that gap by 1, and extend the range by 1. + // the case where the current ackrange doesn't have a gap + // and the packet number is the largest + 1 should have + // been handled when processing the previous ackRange. + assert ackRange.gap > 0; + var gap = ackRange.gap - 1; + var range = ackRange.range + 1; + var replaced = AckRange.of(gap, range); + ackRanges.set(index, replaced); + return this; + } else { + // the packet falls within the gap of this ack range. + // we need to split the ackRange in two... + // + // in the case where we have + // [31,31] [27,27] -> 31, AckRange[g=0, r=0], AckRange[g=2, r=0] + // and we want to acknowledge 29. + // we should end up with: + // [31,31] [29,29] [27,27] -> + // 31, AckRange[g=0, r=0], AckRange[g=0, r=0], AckRange[g=0, r=0] + assert ackRange.gap > 0 : "%s at index (prev:%s, next:%s)" + .formatted(ackRanges, iterator.previousIndex(), iterator.nextIndex()); + assert packetNumber - ackRange.gap -2 <= largest; + + // compute the smallest packet that was acknowledged by the + // previous ackRange. This should be: + var previousSmallest = largest + ackRange.gap + 2; + + // System.out.printf("ack: %s, largest:%s, previousSmallest:%d%n", + // ackRange, largest, previousSmallest); + + // compute the point at which we should split the current ackRange + // the current ackRange will be split in two: first, and second + // - first will replace the current ackRange + // - second will be inserted after first + var firstgap = previousSmallest - packetNumber -2; + AckRange first = AckRange.of(firstgap, 0); + AckRange second = AckRange.of(ackRange.gap - firstgap -2, ackRange.range); + ackRanges.set(index, first); + iterator.add(second); + return this; + } + } else if (packetNumber < smallest) { + // otherwise, if the packet number is smaller than + // the smallest packet acknowledged by the current ackRange, + // there are two cases: + + // If the current ackRange is the last: it's simple! + // But there are again two cases: + if (!iterator.hasNext()) { + // If the packet number we want to acknowledge is just below + // the smallest packet number acknowledge by the current + // ackRange, there is no gap between the packet number and + // the current range, so we can simply extend the current range + // Otherwise, we need to append a new ackRange. + if (packetNumber == smallest - 1) { + // no gap: we can extend the current range + AckRange replaced = AckRange + .of(ackRange.gap, ackRange.range + 1); + ackRanges.set(index, replaced); + } else { + // gap: we need to add a new AckRange + AckRange last = AckRange.of(smallest - packetNumber - 2, 0); + iterator.add(last); + } + return this; + } else if (packetNumber == smallest - 1) { + // Otherwise, if the packet number to be acknowledged is + // just below the smallest packet ackowledged by the current + // range, there are again two cases, depending on + // whether the next ackRange has a gap that can be reduced, + // or not + assert iterator.hasNext(); + AckRange next = ackRanges.get(index + 1); + // if the gap of the next packet can be reduced, that's great! + // just do it! We need to reduce that gap by one, and extend + // the range of the current ackRange + if (next.gap > 0) { + // reduce the gap in the next ackrange, and increase + // the range in the current ackrange. + // System.out.printf("ack: %s, next: %s%n", ackRange, next); + AckRange first = AckRange.of(ackRange.gap, ackRange.range + 1); + AckRange second = AckRange.of(next.gap - 1, next.range); + // System.out.printf("first: %s, second: %s%n", first, second); + ackRanges.set(index, first); + ackRanges.set(index + 1, second); + return this; + } else { + // Otherwise, that's the complex case again. + // we have a gap of 1 packet between 2 ackranges. + // our packet number falls exactly in that gap. + // We need to merge the two ranges! + // merge with next ackRange: remove the current ackRange, + // the ackRange at the current index is now the next ackRange, + // replace it with a merged ACK range. + var mergedRanges = ackRange.range + next.range + 2; + iterator.remove(); + ackRanges.set(index, AckRange.of(ackRange.gap, mergedRanges)); + return this; + } + } + } else { + // Otherwise, the packet is already acknowledged! + // nothing to do. + assert packetNumber <= largest && packetNumber >= smallest; + return this; + } + } + } else { + // already acknowledged! + assert packetNumber == largestAcknowledged; + return this; + } + return this; + } + + /** + * {@return true if this builder contains no ACK yet} + */ + public boolean isEmpty() { + return ackRanges.isEmpty(); + } + + /** + * {@return the number of ACK ranges in this builder, including the fake + * first ACK range} + */ + public int length() { + return ackRanges.size(); + } + + /** + * {@return true if the given packet number is already acknowledged + * by this builder} + * @param packetNumber a packet number + */ + public boolean isAcknowledging(long packetNumber) { + if (isEmpty()) return false; + return AckFrame.isAcknowledging(largestAcknowledged, ackRanges, packetNumber); + } + + /** + * {@return the smallest packet number acknowledged by this {@code AckFrame}} + */ + public long smallestAcknowledged() { + if (largestAcknowledged == -1L) return -1L; + return AckFrame.smallestAcknowledged(largestAcknowledged, ackRanges); + } + + // drop acknowledgement of all packet numbers acknowledged + // by AckRange instances coming after the given index, and + // return the smallest packet number now acked by this + // AckFrame. + private long dropRangesIfAfter(int ackIndex) { + assert ackIndex > 0 && ackIndex < ackRanges.size(); + long largest = largestAcknowledged; + long smallest = largest + 2; + ListIterator iterator = ackRanges.listIterator(); + int index = -1; + boolean removeRemainings = false; + long newLargestAckAcked = -1; + while (iterator.hasNext()) { + if (index == ackIndex) { + newLargestAckAcked = smallest; + removeRemainings = true; + } + AckRange ackRange = iterator.next(); + if (removeRemainings) { + iterator.remove(); + continue; + } + index++; + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + } + return newLargestAckAcked; + } + + + // drop acknowledgement of all packet numbers less or equal + // to `largestAckAcked; + private AckFrameBuilder dropIfSmallerThan(long largestAckAcked) { + if (largestAckAcked >= largestAcknowledged) { + largestAcknowledged = -1; + ackRanges.clear(); + return this; + } + long largest = largestAcknowledged; + long smallest = largest + 2; + ListIterator iterator = ackRanges.listIterator(); + int index = -1; + boolean removeRemainings = false; + while (iterator.hasNext()) { + AckRange ackRange = iterator.next(); + if (removeRemainings) { + iterator.remove(); + continue; + } + index++; + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + if (largest <= largestAckAcked) { + iterator.remove(); + removeRemainings = true; + } else if (smallest <= largestAckAcked) { + long removed = largestAckAcked - smallest + 1; + long gap = ackRange.gap; + long range = ackRange.range - removed; + assert gap >= 0; + assert range >= 0; + ackRanges.set(index, new AckRange(gap, range)); + removeRemainings = true; + } + } + return this; + } + + /** + * Builds an {@code AckFrame} from this builder's content. + * @return a new {@code AckFrame}. + */ + public AckFrame build() { + return new AckFrame(largestAcknowledged, ackDelay, ackRanges, + ect0Count, ect1Count, ecnCECount); + } + + private AckFrameBuilder acknowledgeFirstPacket(long packetNumber) { + assert ackRanges.isEmpty(); + largestAcknowledged = packetNumber; + ackRanges.add(AckRange.INITIAL); + return this; + } + + private AckFrameBuilder acknowledgeLargerPacket(long largerThanLargest) { + var packetNumber = largerThanLargest; + // the new packet is larger than the largest acknowledged + var firstAckRange = ackRanges.get(0); + if (largestAcknowledged == packetNumber -1) { + // if packetNumber is largestAcknowledged + 1, we can simply + // extend the first ack range by 1 + firstAckRange = AckRange.of(0, firstAckRange.range + 1); + ackRanges.set(0, firstAckRange); + } else { + // otherwise - we have a gap - we need to acknowledge the new packetNumber, + // and then add the gap that separate it from the previous largestAcknowledged... + ackRanges.add(0, AckRange.INITIAL); // acknowledge packetNumber only + long gap = packetNumber - largestAcknowledged -2; + var secondAckRange = AckRange.of(gap, firstAckRange.range); + ackRanges.set(1, secondAckRange); // add the gap + } + largestAcknowledged = packetNumber; + return this; + } + + public static AckFrameBuilder ofNullable(AckFrame frame) { + return frame == null ? new AckFrameBuilder() : new AckFrameBuilder(frame); + } + + } + + // This is described in RFC 9000, Section 19.3.1 ACK Ranges + // https://www.rfc-editor.org/rfc/rfc9000#name-ack-ranges + private static boolean isAcknowledging(long largestAcknowledged, + List ackRanges, + long packetNumber) { + if (packetNumber > largestAcknowledged) return false; + var largest = largestAcknowledged; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + if (packetNumber > largest) return false; + smallest = largest - ackRange.range; + if (packetNumber >= smallest) return true; + } + return false; + } + + private static boolean isRangeAcknowledged(long largestAcknowledged, + List ackRanges, + long first, + long last) { + assert last >= first; + if (last > largestAcknowledged) return false; + var largest = largestAcknowledged; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + if (last > largest) return false; + smallest = largest - ackRange.range; + if (first >= smallest) return true; + } + return false; + } + + // This is described in RFC 9000, Section 19.3.1 ACK Ranges + // https://www.rfc-editor.org/rfc/rfc9000#name-ack-ranges + private static long smallestAcknowledged(long largestAcknowledged, + List ackRanges) { + long largest = largestAcknowledged; + long smallest = largest + 2; + assert !ackRanges.isEmpty(); + for (AckRange ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + } + return smallest; + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java new file mode 100644 index 00000000000..5e0f2abd8df --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * A CONNECTION_CLOSE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class ConnectionCloseFrame extends QuicFrame { + + /** + * This variant indicates an error originating from the higher + * level protocol, for instance, HTTP/3. + */ + public static final int CONNECTION_CLOSE_VARIANT = 0x1d; + private final long errorCode; + private final long errorFrameType; + private final boolean variant; + private final byte[] reason; + private String cachedToString; + private String cachedReason; + + /** + * An immutable ConnectionCloseFrame of type 0x1c with no reason phrase + * and an error of type APPLICATION_ERROR. + * @apiNote + * From + * RFC 9000 - section 10.2.3: + *

    + * A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE + * of type 0x1c when sending the frame in Initial or Handshake packets. + * Otherwise, information about the application state might be revealed. + * Endpoints MUST clear the value of the Reason Phrase field and SHOULD + * use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE + * of type 0x1c. + *
    + */ + public static final ConnectionCloseFrame APPLICATION_ERROR = + new ConnectionCloseFrame(QuicTransportErrors.APPLICATION_ERROR.code(), 0,""); + + /** + * Incoming CONNECTION_CLOSE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + ConnectionCloseFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(CONNECTION_CLOSE); + errorCode = decodeVLField(buffer, "errorCode"); + if (type == CONNECTION_CLOSE) { + variant = false; + errorFrameType = decodeVLField(buffer, "errorFrameType"); + } else { + assert type == CONNECTION_CLOSE_VARIANT; + errorFrameType = -1; + variant = true; + } + int reasonLength = decodeVLFieldAsInt(buffer, "reasonLength"); + validateRemainingLength(buffer, reasonLength, type); + reason = new byte[reasonLength]; + buffer.get(reason, 0, reasonLength); + } + + /** + * Outgoing CONNECTION_CLOSE frame (variant with errorFrameType - 0x1c). + * This indicates a {@linkplain jdk.internal.net.quic.QuicTransportErrors + * quic transport error}. + */ + public ConnectionCloseFrame(long errorCode, long errorFrameType, String reason) { + super(CONNECTION_CLOSE); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.errorFrameType = requireVLRange(errorFrameType, "errorFrameType"); + this.variant = false; + this.cachedReason = reason; + this.reason = getReasonBytes(reason); + } + + /** + * Outgoing CONNECTION_CLOSE frame (variant without errorFrameType). + * This indicates an error originating from the higher level protocol, + * for instance {@linkplain jdk.internal.net.http.http3.Http3Error HTTP/3}. + */ + public ConnectionCloseFrame(long errorCode, String reason) { + super(CONNECTION_CLOSE); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.errorFrameType = -1; + this.variant = true; + this.cachedReason = reason; + this.reason = getReasonBytes(reason); + } + + private static byte[] getReasonBytes(String reason) { + return reason != null ? + reason.getBytes(StandardCharsets.UTF_8) : + new byte[0]; + } + + /** + * {@return a ConnectionCloseFrame suitable for inclusion in + * an Initial or Handshake packet} + */ + public ConnectionCloseFrame clearApplicationState() { + return this.variant ? APPLICATION_ERROR : this; + } + + @Override + public long getTypeField() { + return variant ? CONNECTION_CLOSE_VARIANT : CONNECTION_CLOSE; + } + + @Override + public boolean isAckEliciting() { + return false; + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, errorCode, "errorCode"); + if (!variant) { + encodeVLField(buffer, errorFrameType, "errorFrameType"); + } + encodeVLField(buffer, reason.length, "reasonLength"); + if (reason.length > 0) { + buffer.put(reason); + } + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(errorCode) + + (variant ? 0 : getVLFieldLengthFor(errorFrameType)) + + getVLFieldLengthFor(reason.length) + + reason.length; + } + + public long errorCode() { + return errorCode; + } + + public long errorFrameType() { + return errorFrameType; + } + + public boolean variant() { + return variant; + } + + public boolean isQuicTransportCode() { + return !variant; + } + + public boolean isApplicationCode() { + return variant; + } + + public byte[] reason() { + return reason; + } + + public String reasonString() { + if (cachedReason != null) return cachedReason; + if (reason == null) return null; + if (reason.length == 0) return ""; + return cachedReason = new String(reason, StandardCharsets.UTF_8); + } + + @Override + public String toString() { + if (cachedToString == null) { + final StringBuilder sb = new StringBuilder("ConnectionCloseFrame[type=0x"); + final long type = getTypeField(); + sb.append(Long.toHexString(type)) + .append(", errorCode=0x").append(Long.toHexString(errorCode)); + // CRYPTO_ERROR codes ranging 0x0100-0x01ff + if (type == 0x1c) { + if (errorCode >= 0x0100 && errorCode <= 0x01ff) { + // this represents a CRYPTO_ERROR which as per RFC-9001, section 4.8: + // A TLS alert is converted into a QUIC connection error. The AlertDescription + // value is added to 0x0100 to produce a QUIC error code from the range reserved for + // CRYPTO_ERROR; ... The resulting value is sent in a QUIC CONNECTION_CLOSE + // frame of type 0x1c + + // find the tls alert code from the error code, by substracting 0x0100 from + // the error code + sb.append(", tlsAlertDescription=").append(errorCode - 0x0100); + } + sb.append(", errorFrameType=0x").append(Long.toHexString(errorFrameType)); + } + if (cachedReason == null) { + cachedReason = new String(reason, StandardCharsets.UTF_8); + } + sb.append(", reason=").append(cachedReason).append("]"); + + cachedToString = sb.toString(); + } + return cachedToString; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java new file mode 100644 index 00000000000..7b606527193 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A CRYPTO Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class CryptoFrame extends QuicFrame { + + private final long offset; + private final int length; + private final ByteBuffer cryptoData; + + CryptoFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(CRYPTO); + offset = decodeVLField(buffer, "offset"); + length = decodeVLFieldAsInt(buffer, "length"); + if (offset + length > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new QuicTransportException("Maximum crypto offset exceeded", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length, type); + int pos = buffer.position(); + // The buffer is the datagram: we will make a copy if the datagram + // is larger than the crypto frame by 64 bytes. + cryptoData = Utils.sliceOrCopy(buffer, pos, length, 64); + buffer.position(pos + length); + } + + /** + * Creates CryptoFrame + */ + public CryptoFrame(long offset, int length, ByteBuffer cryptoData) { + this(offset, length, cryptoData, true); + } + + private CryptoFrame(long offset, int length, ByteBuffer cryptoData, boolean slice) + { + super(CRYPTO); + this.offset = requireVLRange(offset, "offset"); + if (length != cryptoData.remaining()) + throw new IllegalArgumentException("bad length: " + length); + this.length = length; + this.cryptoData = slice + ? cryptoData.slice(cryptoData.position(), length) + : cryptoData; + } + + /** + * Creates a new CryptoFrame which is a slice of this crypto frame. + * @param offset the new offset + * @param length the new length + * @return a slice of the current crypto frame + * @throws IndexOutOfBoundsException if the offset or length + * exceed the bounds of this crypto frame + */ + public CryptoFrame slice(long offset, int length) { + long offsetdiff = offset - offset(); + long oldlen = length(); + Objects.checkFromIndexSize(offsetdiff, length, oldlen); + int pos = cryptoData.position(); + // safe cast to int since offsetdiff < length + int newpos = Math.addExact(pos, (int)offsetdiff); + ByteBuffer slice = Utils.sliceOrCopy(cryptoData, newpos, length); + return new CryptoFrame(offset, length, slice, false); + } + + @Override + public void encode(ByteBuffer dest) { + if (size() > dest.remaining()) { + throw new BufferOverflowException(); + } + int pos = dest.position(); + encodeVLField(dest, CRYPTO, "type"); + encodeVLField(dest, offset, "offset"); + encodeVLField(dest, length, "length"); + assert cryptoData.remaining() == length; + putByteBuffer(dest, cryptoData); + assert dest.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(CRYPTO) + + getVLFieldLengthFor(offset) + + getVLFieldLengthFor(length) + + length; + } + + /** + * {@return the frame offset} + */ + public long offset() { + return offset; + } + + public int length() { + return length; + } + + /** + * {@return the frame payload} + */ + public ByteBuffer payload() { + return cryptoData.slice(); + } + + @Override + public String toString() { + return "CryptoFrame(" + + "offset=" + offset + + ", length=" + length + + ')'; + } + + public static int compareOffsets(CryptoFrame cf1, CryptoFrame cf2) { + return Long.compare(cf1.offset, cf2.offset); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java new file mode 100644 index 00000000000..b008e643cda --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A DATA_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class DataBlockedFrame extends QuicFrame { + + private final long maxData; + + /** + * Incoming DATA_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + DataBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(DATA_BLOCKED); + maxData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing DATA_BLOCKED frame + */ + public DataBlockedFrame(long maxData) { + super(DATA_BLOCKED); + this.maxData = requireVLRange(maxData, "maxData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, DATA_BLOCKED, "type"); + encodeVLField(buffer, maxData, "maxData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxData() { + return maxData; + } + + @Override + public int size() { + return getVLFieldLengthFor(DATA_BLOCKED) + + getVLFieldLengthFor(maxData); + } + + @Override + public String toString() { + return "DataBlockedFrame(" + + "maxData=" + maxData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java new file mode 100644 index 00000000000..ffe6aff2f0d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A HANDSHAKE_DONE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class HandshakeDoneFrame extends QuicFrame { + + /** + * Incoming HANDSHAKE_DONE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + */ + HandshakeDoneFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(HANDSHAKE_DONE); + } + + /** + * Outgoing HANDSHAKE_DONE frame + */ + public HandshakeDoneFrame() { + super(HANDSHAKE_DONE); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, HANDSHAKE_DONE, "type"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(HANDSHAKE_DONE); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java new file mode 100644 index 00000000000..26720c34494 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RESET_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxDataFrame extends QuicFrame { + + private final long maxData; + + /** + * Incoming MAX_DATA frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxDataFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_DATA); + maxData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing MAX_DATA frame + */ + public MaxDataFrame(long maxData) { + super(MAX_DATA); + this.maxData = requireVLRange(maxData, "maxData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, MAX_DATA, "type"); + encodeVLField(buffer, maxData, "maxData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxData() { + return maxData; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_DATA) + + getVLFieldLengthFor(maxData); + } + + @Override + public String toString() { + return "MaxDataFrame(" + + "maxData=" + maxData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java new file mode 100644 index 00000000000..3fff70a377c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A MAX_STREAM_DATA Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxStreamDataFrame extends QuicFrame { + + private final long streamID; + private final long maxStreamData; + + /** + * Incoming MAX_STREAM_DATA frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxStreamDataFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_STREAM_DATA); + streamID = decodeVLField(buffer, "streamID"); + maxStreamData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing MAX_STREAM_DATA frame + */ + public MaxStreamDataFrame(long streamID, long maxStreamData) { + super(MAX_STREAM_DATA); + this.streamID = requireVLRange(streamID, "streamID"); + this.maxStreamData = requireVLRange(maxStreamData, "maxStreamData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, MAX_STREAM_DATA, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, maxStreamData, "maxStreamData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreamData() { + return maxStreamData; + } + + public long streamID() { + return streamID; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_STREAM_DATA) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(maxStreamData); + } + + @Override + public String toString() { + return "MaxStreamDataFrame(" + + "streamId=" + streamID + + ", maxStreamData=" + maxStreamData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java new file mode 100644 index 00000000000..e35b16195a6 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A MAX_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxStreamsFrame extends QuicFrame { + static final long MAX_VALUE = 1L << 60; + + private final long maxStreams; + private final boolean bidi; + + /** + * Incoming MAX_STREAMS frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxStreamsFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_STREAMS); + bidi = (type == MAX_STREAMS); + maxStreams = decodeVLField(buffer, "maxStreams"); + if (maxStreams > MAX_VALUE) { + throw new QuicTransportException("Invalid maximum streams", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * Outgoing MAX_STREAMS frame + */ + public MaxStreamsFrame(boolean bidi, long maxStreams) { + super(MAX_STREAMS); + this.bidi = bidi; + this.maxStreams = requireVLRange(maxStreams, "maxStreams"); + } + + @Override + public long getTypeField() { + return MAX_STREAMS + (bidi?0:1); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, maxStreams, "maxStreams"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreams() { + return maxStreams; + } + + public boolean isBidi() { + return bidi; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_STREAMS) + + getVLFieldLengthFor(maxStreams); + } + + @Override + public String toString() { + return "MaxStreamsFrame(bidi=" + bidi + + ", maxStreams=" + maxStreams + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java new file mode 100644 index 00000000000..87e91b21e75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A NEW_CONNECTION_ID Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class NewConnectionIDFrame extends QuicFrame { + + private final long sequenceNumber; + private final long retirePriorTo; + private final ByteBuffer connectionId; + private final ByteBuffer statelessResetToken; + + /** + * Incoming NEW_CONNECTION_ID frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + NewConnectionIDFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(NEW_CONNECTION_ID); + sequenceNumber = decodeVLField(buffer, "sequenceNumber"); + retirePriorTo = decodeVLField(buffer, "retirePriorTo"); + if (retirePriorTo > sequenceNumber) { + throw new QuicTransportException("Invalid retirePriorTo", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, 17, type); + int length = Byte.toUnsignedInt(buffer.get()); + if (length < 1 || length > 20) { + throw new QuicTransportException("Invalid connection ID", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length + 16, type); + int position = buffer.position(); + connectionId = buffer.slice(position, length); + position += length; + statelessResetToken = buffer.slice(position, 16); + position += 16; + buffer.position(position); + } + + /** + * Outgoing NEW_CONNECTION_ID frame + */ + public NewConnectionIDFrame(long sequenceNumber, long retirePriorTo, ByteBuffer connectionId, ByteBuffer statelessResetToken) { + super(NEW_CONNECTION_ID); + this.sequenceNumber = requireVLRange(sequenceNumber, "sequenceNumber"); + this.retirePriorTo = requireVLRange(retirePriorTo, "retirePriorTo"); + int length = connectionId.remaining(); + if (length < 1 || length > 20) + throw new IllegalArgumentException("invalid length"); + this.connectionId = connectionId.slice(); + if (statelessResetToken.remaining() != 16) + throw new IllegalArgumentException("stateless reset token must be 16 bytes"); + this.statelessResetToken = statelessResetToken.slice(); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, NEW_CONNECTION_ID, "type"); + encodeVLField(buffer, sequenceNumber, "sequenceNumber"); + encodeVLField(buffer, retirePriorTo, "retirePriorTo"); + int length = connectionId.remaining(); + buffer.put((byte)length); + putByteBuffer(buffer, connectionId); + putByteBuffer(buffer, statelessResetToken); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(NEW_CONNECTION_ID) + + getVLFieldLengthFor(sequenceNumber) + + getVLFieldLengthFor(retirePriorTo) + + 1 // connection length + + connectionId.remaining() + + statelessResetToken.remaining(); + } + + public long sequenceNumber() { + return sequenceNumber; + } + + public long retirePriorTo() { + return retirePriorTo; + } + + public ByteBuffer connectionId() { + return connectionId; + } + + public ByteBuffer statelessResetToken() { + return statelessResetToken; + } + + @Override + public String toString() { + return "NewConnectionIDFrame(seqNumber=" + sequenceNumber + + ", retirePriorTo=" + retirePriorTo + + ", connIdLength=" + connectionId.remaining() + + ")"; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java new file mode 100644 index 00000000000..08bc72ff1c2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A NEW_TOKEN frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class NewTokenFrame extends QuicFrame { + private final byte[] token; + + /** + * Incoming NEW_TOKEN frame + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + NewTokenFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(NEW_TOKEN); + int length = decodeVLFieldAsInt(buffer, "token length"); + if (length == 0) { + throw new QuicTransportException("Empty token", + null, type, + QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length, type); + final byte[] t = new byte[length]; + buffer.get(t); + this.token = t; + } + + /** + * Outgoing NEW_TOKEN frame whose token is the given ByteBuffer + * (position to limit) + */ + public NewTokenFrame(final ByteBuffer tokenBuf) { + super(NEW_TOKEN); + Objects.requireNonNull(tokenBuf); + final int length = tokenBuf.remaining(); + if (length <= 0) { + throw new IllegalArgumentException("Invalid token length"); + } + final byte[] t = new byte[length]; + tokenBuf.get(t); + this.token = t; + } + + @Override + public void encode(final ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, NEW_TOKEN, "type"); + encodeVLField(buffer, token.length, "token length"); + buffer.put(token); + assert buffer.position() - pos == size(); + } + + public byte[] token() { + return this.token; + } + + @Override + public int size() { + final int tokenLength = token.length; + return getVLFieldLengthFor(NEW_TOKEN) + + getVLFieldLengthFor(tokenLength) + + tokenLength; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java new file mode 100644 index 00000000000..d0258aee84e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * PaddingFrames. Since padding frames comprise a single zero byte + * this class actually represents sequences of PaddingFrames. + * When decoding, the class consumes all the zero bytes that are + * available and when encoding, the number of required padding bytes + * is specified. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PaddingFrame extends QuicFrame { + + private final int size; + + /** + * Incoming + */ + PaddingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PADDING); + int count = 1; + while (buffer.hasRemaining()) { + if (buffer.get() == 0) { + count++; + } else { + int pos = buffer.position(); + buffer.position(pos - 1); + break; + } + } + size = count; + } + + /** + * Outgoing + * @param size the number of padding frames that should be written + * to the buffer. Each frame is one byte long. + */ + public PaddingFrame(int size) { + super(PADDING); + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than zero"); + } + this.size = size; + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + for (int i=0; i buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PATH_CHALLENGE, "type"); + putByteBuffer(buffer, data); + assert buffer.position() - pos == size(); + } + + /** + */ + public ByteBuffer data() { + return data; + } + + @Override + public int size() { + return getVLFieldLengthFor(PATH_CHALLENGE) + LENGTH; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java new file mode 100644 index 00000000000..2b1edcfe731 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A PATH_RESPONSE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PathResponseFrame extends QuicFrame { + + public static final int LENGTH = 8; + private final ByteBuffer data; + + /** + * Incoming PATH_RESPONSE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + PathResponseFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PATH_RESPONSE); + validateRemainingLength(buffer, LENGTH, type); + int position = buffer.position(); + data = buffer.slice(position, LENGTH); + buffer.position(position + LENGTH); + } + + /** + * Outgoing PATH_RESPONSE frame + */ + public PathResponseFrame(ByteBuffer data) { + super(PATH_RESPONSE); + if (data.remaining() != LENGTH) + throw new IllegalArgumentException("response data must be 8 bytes"); + this.data = data.slice(); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PATH_RESPONSE, "type"); + putByteBuffer(buffer, data); + assert buffer.position() - pos == size(); + } + + /** + */ + public ByteBuffer data() { + return data; + } + + @Override + public int size() { + return getVLFieldLengthFor(PATH_RESPONSE) + LENGTH; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java new file mode 100644 index 00000000000..9717a3c31d1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A PING frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PingFrame extends QuicFrame { + + PingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PING); + } + + /** + */ + public PingFrame() { + super(PING); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PING, "type"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(PING); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java new file mode 100644 index 00000000000..f4a230452d4 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.ByteBuffer; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A QUIC Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public abstract sealed class QuicFrame permits + AckFrame, + DataBlockedFrame, + ConnectionCloseFrame, CryptoFrame, + HandshakeDoneFrame, + MaxDataFrame, MaxStreamDataFrame, MaxStreamsFrame, + NewConnectionIDFrame, NewTokenFrame, + PaddingFrame, PathChallengeFrame, PathResponseFrame, PingFrame, + ResetStreamFrame, RetireConnectionIDFrame, + StreamsBlockedFrame, StreamDataBlockedFrame, StreamFrame, StopSendingFrame { + + public static final long MAX_VL_INTEGER = (1L << 62) - 1; + /** + * Frame types + */ + public static final int PADDING=0x00; + public static final int PING=0x01; + public static final int ACK=0x02; + public static final int RESET_STREAM=0x04; + public static final int STOP_SENDING=0x05; + public static final int CRYPTO=0x06; + public static final int NEW_TOKEN=0x07; + public static final int STREAM=0x08; + public static final int MAX_DATA=0x10; + public static final int MAX_STREAM_DATA=0x11; + public static final int MAX_STREAMS=0x12; + public static final int DATA_BLOCKED=0x14; + public static final int STREAM_DATA_BLOCKED=0x15; + public static final int STREAMS_BLOCKED=0x16; + public static final int NEW_CONNECTION_ID=0x18; + public static final int RETIRE_CONNECTION_ID=0x19; + public static final int PATH_CHALLENGE=0x1a; + public static final int PATH_RESPONSE=0x1b; + public static final int CONNECTION_CLOSE=0x1c; + public static final int HANDSHAKE_DONE=0x1e; + private static final int MAX_KNOWN_FRAME_TYPE = HANDSHAKE_DONE; + private final int frameType; + + /** + * Concrete Frame types normally have two constructors which call this + * + * XXXFrame(ByteBuffer, int firstByte) which is called for incoming frames + * after reading the first byte to determine the type. The firstByte is also + * supplied to the constructor because it can contain additional state information + * + * XXXFrame(...) which is called to instantiate outgoing frames + * @param type the first byte of the frame, which encodes the frame type. + */ + QuicFrame(int type) { + frameType = type; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * decode given ByteBuffer and return a QUICFrame + */ + public static QuicFrame decode(ByteBuffer buffer) throws QuicTransportException { + long frameTypeLong = VariableLengthEncoder.decode(buffer); + if (frameTypeLong < 0) { + throw new QuicTransportException("Error decoding frame type", + null, 0, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + if (frameTypeLong > Integer.MAX_VALUE) { + throw new QuicTransportException("Unrecognized frame", + null, frameTypeLong, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + int frameType = (int)frameTypeLong; + var frame = switch (maskType(frameType)) { + case ACK -> new AckFrame(buffer, frameType); + case STREAM -> new StreamFrame(buffer, frameType); + case RESET_STREAM -> new ResetStreamFrame(buffer, frameType); + case PADDING -> new PaddingFrame(buffer, frameType); + case PING -> new PingFrame(buffer, frameType); + case STOP_SENDING -> new StopSendingFrame(buffer, frameType); + case CRYPTO -> new CryptoFrame(buffer, frameType); + case NEW_TOKEN -> new NewTokenFrame(buffer, frameType); + case DATA_BLOCKED -> new DataBlockedFrame(buffer, frameType); + case MAX_DATA -> new MaxDataFrame(buffer, frameType); + case MAX_STREAMS -> new MaxStreamsFrame(buffer, frameType); + case MAX_STREAM_DATA -> new MaxStreamDataFrame(buffer, frameType); + case STREAM_DATA_BLOCKED -> new StreamDataBlockedFrame(buffer, frameType); + case STREAMS_BLOCKED -> new StreamsBlockedFrame(buffer, frameType); + case NEW_CONNECTION_ID -> new NewConnectionIDFrame(buffer, frameType); + case RETIRE_CONNECTION_ID -> new RetireConnectionIDFrame(buffer, frameType); + case PATH_CHALLENGE -> new PathChallengeFrame(buffer, frameType); + case PATH_RESPONSE -> new PathResponseFrame(buffer, frameType); + case CONNECTION_CLOSE -> new ConnectionCloseFrame(buffer, frameType); + case HANDSHAKE_DONE -> new HandshakeDoneFrame(buffer, frameType); + default -> throw new QuicTransportException("Unrecognized frame", + null, frameType, QuicTransportErrors.FRAME_ENCODING_ERROR); + }; + assert frameClassOf(maskType(frameType)) == frame.getClass(); + assert frameTypeOf(frame.getClass()) == maskType(frameType); + assert frame.getTypeField() == frameType : "frame type mismatch: " + + frameType + "!=" + frame.getTypeField() + + " for frame: " + frame; + return frame; + } + + public static Class frameClassOf(int frameType) { + return switch (maskType(frameType)) { + case ACK -> AckFrame.class; + case STREAM -> StreamFrame.class; + case RESET_STREAM -> ResetStreamFrame.class; + case PADDING -> PaddingFrame.class; + case PING -> PingFrame.class; + case STOP_SENDING -> StopSendingFrame.class; + case CRYPTO -> CryptoFrame.class; + case NEW_TOKEN -> NewTokenFrame.class; + case DATA_BLOCKED -> DataBlockedFrame.class; + case MAX_DATA -> MaxDataFrame.class; + case MAX_STREAMS -> MaxStreamsFrame.class; + case MAX_STREAM_DATA -> MaxStreamDataFrame.class; + case STREAM_DATA_BLOCKED -> StreamDataBlockedFrame.class; + case STREAMS_BLOCKED -> StreamsBlockedFrame.class; + case NEW_CONNECTION_ID -> NewConnectionIDFrame.class; + case RETIRE_CONNECTION_ID -> RetireConnectionIDFrame.class; + case PATH_CHALLENGE -> PathChallengeFrame.class; + case PATH_RESPONSE -> PathResponseFrame.class; + case CONNECTION_CLOSE -> ConnectionCloseFrame.class; + case HANDSHAKE_DONE -> HandshakeDoneFrame.class; + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + } + + public static int frameTypeOf(Class frameClass) { + // we don't have class pattern matching yet - so switch + // on the class name instead + return switch (frameClass.getSimpleName()) { + case "AckFrame" -> ACK; + case "StreamFrame" -> STREAM; + case "ResetStreamFrame" -> RESET_STREAM; + case "PaddingFrame" -> PADDING; + case "PingFrame" -> PING; + case "StopSendingFrame" -> STOP_SENDING; + case "CryptoFrame" -> CRYPTO; + case "NewTokenFrame" -> NEW_TOKEN; + case "DataBlockedFrame" -> DATA_BLOCKED; + case "MaxDataFrame" -> MAX_DATA; + case "MaxStreamsFrame" -> MAX_STREAMS; + case "MaxStreamDataFrame" -> MAX_STREAM_DATA; + case "StreamDataBlockedFrame" -> STREAM_DATA_BLOCKED; + case "StreamsBlockedFrame" -> STREAMS_BLOCKED; + case "NewConnectionIDFrame" -> NEW_CONNECTION_ID; + case "RetireConnectionIDFrame" -> RETIRE_CONNECTION_ID; + case "PathChallengeFrame" -> PATH_CHALLENGE; + case "PathResponseFrame" -> PATH_RESPONSE; + case "ConnectionCloseFrame" -> CONNECTION_CLOSE; + case "HandshakeDoneFrame" -> HANDSHAKE_DONE; + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + } + + /** + * Writes src to dest, preserving position in src + */ + protected static void putByteBuffer(ByteBuffer dest, ByteBuffer src) { + dest.put(src.asReadOnlyBuffer()); + } + + /** + * Throws a QuicTransportException if the given buffer does not have enough bytes + * to finish decoding the frame + * + * @param buffer source buffer + * @param expected minimum number of bytes required + * @param type frame type to include in exception + * @throws QuicTransportException if the buffer is shorter than {@code expected} + */ + protected static void validateRemainingLength(ByteBuffer buffer, int expected, long type) + throws QuicTransportException + { + if (buffer.remaining() < expected) { + throw new QuicTransportException("Error decoding frame", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * depending on the frame type, additional bits can be encoded + * in frameType(). This masks them out to return a unique value + * for each frame type. + */ + private static int maskType(int type) { + if (type >= ACK && type < RESET_STREAM) + return ACK; + if (type >= STREAM && type < MAX_DATA) + return STREAM; + if (type >= MAX_STREAMS && type < DATA_BLOCKED) + return MAX_STREAMS; + if (type >= STREAMS_BLOCKED && type < NEW_CONNECTION_ID) + return STREAMS_BLOCKED; + if (type >= CONNECTION_CLOSE && type < HANDSHAKE_DONE) + return CONNECTION_CLOSE; + // all others are unique + return type; + } + + /** + * {@return true if this frame is ACK-eliciting} + * A frame is ACK-eliciting if it is anything + * other than {@link QuicFrame#ACK}, + * {@link QuicFrame#PADDING} or + * {@link QuicFrame#CONNECTION_CLOSE} + * (or its variant). + */ + public boolean isAckEliciting() { return true; } + + /** + * {@return the minimum number of bytes needed to encode this frame} + */ + public abstract int size(); + + protected final long decodeVLField(ByteBuffer buffer, String name) throws QuicTransportException { + long v = VariableLengthEncoder.decode(buffer); + if (v < 0) { + throw new QuicTransportException("Error decoding field: " + name, + null, getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + return v; + } + + protected final int decodeVLFieldAsInt(ByteBuffer buffer, String name) throws QuicTransportException { + long l = decodeVLField(buffer, name); + int intval = (int)l; + if (((long)intval) != l) { + throw new QuicTransportException(name + ":field too long", + null, getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + return intval; + } + + protected static int requireVLRange(int val, String message) { + if (val < 0) { + throw new IllegalArgumentException(message + " " + val + " not in range"); + } + return val; + } + + protected static long requireVLRange(long val, String fieldName) { + if (val < 0 || val > MAX_VL_INTEGER) { + throw new IllegalArgumentException( + String.format("%s not in VL range: %s", fieldName, val)); + } + return val; + } + + protected static void encodeVLField(ByteBuffer buffer, long val, String name) { + try { + VariableLengthEncoder.encode(buffer, val); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error encoding " + name, e); + } + } + + protected static int getVLFieldLengthFor(long val) { + return VariableLengthEncoder.getEncodedSize(val); + } + + /** + * The type of this frame, ie. one of values above, which means it + * excludes the additional information that is encoded into the first field + * of some QUIC frames. That additional info has to be maintained by the sub + * class and used by its encode() method to generate the first field for outgoing frames. + * + * see maskType() below + */ + public int frameType() { + return frameType; + } + + /** + * Encode this QUIC Frame into given ByteBuffer + */ + public abstract void encode(ByteBuffer buffer); + + /** + * {@return the type field that was / should be encoded} + * This is the {@linkplain #frameType() frame type} with + * possibly some additional bits set, depending on the + * frame. + * @implSpec + * The default implementation of this method is to return + * {@link #frameType()}. + */ + public long getTypeField() { return frameType(); } + + /** + * Tells whether this particular frame is valid in the given + * packet type. + * + *

    From + * RFC 9000, section 12.5. Frames and Number Spaces: + *

    + * Some frames are prohibited in different packet number space + * The rules here generalize those of TLS, in that frames associated + * with establishing the connection can usually appear in packets + * in any packet number space, whereas those associated with transferring + * data can only appear in the application data packet number space: + * + *
      + *
    • PADDING, PING, and CRYPTO frames MAY appear in any packet number + * space.
    • + *
    • CONNECTION_CLOSE frames signaling errors at the QUIC layer (type 0x1c) + * MAY appear in any packet number space.
    • + *
    • CONNECTION_CLOSE frames signaling application errors (type 0x1d) + * MUST only appear in the application data packet number space. + *
    • ACK frames MAY appear in any packet number space but can only + * acknowledge packets that appeared in that packet number space. + * However, as noted below, 0-RTT packets cannot contain ACK frames.
    • + *
    • All other frame types MUST only be sent in the application data + * packet number space.
    • + *
    + * + * Note that it is not possible to send the following frames in 0-RTT + * packets for various reasons: ACK, CRYPTO, HANDSHAKE_DONE, NEW_TOKEN, + * PATH_RESPONSE, and RETIRE_CONNECTION_ID. A server MAY treat receipt + * of these frames in 0-RTT packets as a connection error of + * type PROTOCOL_VIOLATION. + *
    + * + * @param packetType the packet type + * @return true if the frame can be embedded in a packet of that type + */ + public boolean isValidIn(QuicPacket.PacketType packetType) { + return switch (frameType) { + case PADDING, PING -> true; + case ACK, CRYPTO -> switch (packetType) { + case VERSIONS, ZERORTT -> false; + default -> true; + }; + case CONNECTION_CLOSE -> { + if ((getTypeField() & 0x1D) == 0x1C) yield true; + yield QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + } + case HANDSHAKE_DONE, NEW_TOKEN, PATH_RESPONSE, + RETIRE_CONNECTION_ID -> switch (packetType) { + case ZERORTT -> false; + default -> QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + }; + default -> QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java new file mode 100644 index 00000000000..7a3579fc831 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RESET_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class ResetStreamFrame extends QuicFrame { + + private final long streamID; + private final long errorCode; + private final long finalSize; + + ResetStreamFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(RESET_STREAM); + streamID = decodeVLField(buffer, "streamID"); + errorCode = decodeVLField(buffer, "errorCode"); + finalSize = decodeVLField(buffer, "finalSize"); + } + + /** + */ + public ResetStreamFrame( + long streamID, + long errorCode, + long finalSize) + { + super(RESET_STREAM); + this.streamID = requireVLRange(streamID, "streamID"); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.finalSize = requireVLRange(finalSize, "finalSize"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, RESET_STREAM, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, errorCode, "errorCode"); + encodeVLField(buffer, finalSize, "finalSize"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long streamId() { + return streamID; + } + + /** + */ + public long errorCode() { + return errorCode; + } + + /** + */ + public long finalSize() { + return finalSize; + } + + @Override + public int size() { + return getVLFieldLengthFor(RESET_STREAM) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(errorCode) + + getVLFieldLengthFor(finalSize); + } + + @Override + public String toString() { + return "ResetStreamFrame(stream=" + streamID + + ", errorCode=" + errorCode + + ", finalSize=" + finalSize + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java new file mode 100644 index 00000000000..bf448f6a301 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RETIRE_CONNECTION_ID Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class RetireConnectionIDFrame extends QuicFrame { + + private final long sequenceNumber; + + /** + * Incoming RETIRE_CONNECTION_ID frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + RetireConnectionIDFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(RETIRE_CONNECTION_ID); + sequenceNumber = decodeVLField(buffer, "sequenceNumber"); + } + + /** + * Outgoing RETIRE_CONNECTION_ID frame + */ + public RetireConnectionIDFrame(long sequenceNumber) { + super(RETIRE_CONNECTION_ID); + this.sequenceNumber = requireVLRange(sequenceNumber, "sequenceNumber"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, RETIRE_CONNECTION_ID, "type"); + encodeVLField(buffer, sequenceNumber, "sequenceNumber"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long sequenceNumber() { + return sequenceNumber; + } + + @Override + public int size() { + return getVLFieldLengthFor(RETIRE_CONNECTION_ID) + + getVLFieldLengthFor(sequenceNumber); + } + + @Override + public String toString() { + return "RetireConnectionIDFrame(" + + "sequenceNumber=" + sequenceNumber + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java new file mode 100644 index 00000000000..4a7d6525685 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STOP_SENDING Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StopSendingFrame extends QuicFrame { + + private final long streamID; + private final long errorCode; + + StopSendingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STOP_SENDING); + streamID = decodeVLField(buffer, "streamID"); + errorCode = decodeVLField(buffer, "errorCode"); + } + + /** + */ + public StopSendingFrame(long streamID, long errorCode) { + super(STOP_SENDING); + this.streamID = requireVLRange(streamID, "streamID"); + this.errorCode = requireVLRange(errorCode, "errorCode"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, STOP_SENDING, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, errorCode, "errorCode"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long streamID() { + return streamID; + } + + /** + */ + public long errorCode() { + return errorCode; + } + + @Override + public int size() { + return getVLFieldLengthFor(STOP_SENDING) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(errorCode); + } + + @Override + public String toString() { + return "StopSendingFrame(stream=" + streamID + + ", errorCode=" + errorCode + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java new file mode 100644 index 00000000000..7dd95e2278b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STREAM_DATA_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamDataBlockedFrame extends QuicFrame { + + private final long streamId; + private final long maxStreamData; + + /** + * Incoming STREAM_DATA_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + StreamDataBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAM_DATA_BLOCKED); + assert type == STREAM_DATA_BLOCKED : "STREAM_DATA_BLOCKED, unexpected frame type 0x" + + Integer.toHexString(type); + streamId = decodeVLField(buffer, "streamID"); + maxStreamData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing STREAM_DATA_BLOCKED frame + */ + public StreamDataBlockedFrame(long streamId, long maxStreamData) { + super(STREAM_DATA_BLOCKED); + this.streamId = requireVLRange(streamId, "streamID"); + this.maxStreamData = requireVLRange(maxStreamData, "maxStreamData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, STREAM_DATA_BLOCKED, "type"); + encodeVLField(buffer, streamId, "streamID"); + encodeVLField(buffer, maxStreamData, "maxStreamData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreamData() { + return maxStreamData; + } + + public long streamId() { + return streamId; + } + + @Override + public int size() { + return getVLFieldLengthFor(STREAM_DATA_BLOCKED) + + getVLFieldLengthFor(streamId) + + getVLFieldLengthFor(maxStreamData); + } + + @Override + public String toString() { + return "StreamDataBlockedFrame(" + + "streamId=" + streamId + + ", maxStreamData=" + maxStreamData + + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StreamDataBlockedFrame that)) return false; + if (streamId != that.streamId) return false; + return maxStreamData == that.maxStreamData; + } + + @Override + public int hashCode() { + int result = (int) (streamId ^ (streamId >>> 32)); + result = 31 * result + (int) (maxStreamData ^ (maxStreamData >>> 32)); + return result; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java new file mode 100644 index 00000000000..b597e53c06c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamFrame extends QuicFrame { + + // Flags in frameType() + private static final int OFF = 0x4; + private static final int LEN = 0x2; + private static final int FIN = 0x1; + + private final long streamID; + // true if the OFF bit in the type field has been set + private final boolean typeFieldHasOFF; + private final long offset; + private final int length; // -1 means consume all data in packet + private final int dataLength; + private final ByteBuffer streamData; + private final boolean fin; + + StreamFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAM); + streamID = decodeVLField(buffer, "streamID"); + if ((type & OFF) > 0) { + typeFieldHasOFF = true; + offset = decodeVLField(buffer, "offset"); + } else { + typeFieldHasOFF = false; + offset = 0; + } + if ((type & LEN) > 0) { + length = decodeVLFieldAsInt(buffer, "length"); + } else { + length = -1; + } + if (length == -1) { + int remaining = buffer.remaining(); + streamData = Utils.sliceOrCopy(buffer, buffer.position(), remaining); + buffer.position(buffer.limit()); + dataLength = remaining; + } else { + validateRemainingLength(buffer, length, type); + int pos = buffer.position(); + streamData = Utils.sliceOrCopy(buffer, pos, length); + buffer.position(pos + length); + dataLength = length; + } + fin = (type & FIN) == 1; + } + + /** + * Creates StreamFrame (length == -1 means no length specified in frame + * and is assumed to occupy the remainder of the Quic/UDP packet. + * If a length is specified then it must correspond with the remaining bytes + * in streamData + */ + // It would be interesting to have a version of this constructor that can take + // a list of ByteBuffer. + public StreamFrame(long streamID, long offset, int length, boolean fin, ByteBuffer streamData) { + this(streamID, offset, length, fin, streamData, true); + } + + private StreamFrame(long streamID, long offset, int length, boolean fin, ByteBuffer streamData, boolean slice) + { + super(STREAM); + this.streamID = requireVLRange(streamID, "streamID"); + this.offset = requireVLRange(offset, "offset"); + // if offset is non-zero then we mark that the type field has OFF bit set + // to allow for that bit to be set when encoding this frame + this.typeFieldHasOFF = this.offset != 0; + if (length != -1 && length != streamData.remaining()) { + throw new IllegalArgumentException("bad length"); + } + this.length = length; + this.dataLength = streamData.remaining(); + this.fin = fin; + this.streamData = slice + ? streamData.slice(streamData.position(), dataLength) + : streamData; + } + + /** + * Creates a new StreamFrame which is a slice of this stream frame. + * @param offset the new offset + * @param length the new length + * @return a slice of the current stream frame + * @throws IndexOutOfBoundsException if the offset or length + * exceed the bounds of this stream frame + */ + public StreamFrame slice(long offset, int length) { + long oldoffset = offset(); + long offsetdiff = offset - oldoffset; + long oldlen = dataLength(); + Objects.checkFromIndexSize(offsetdiff, length, oldlen); + int pos = streamData.position(); + // safe cast to int since offsetdiff < length + int newpos = Math.addExact(pos, (int)offsetdiff); + // preserves the FIN bit if set + boolean fin = this.fin && offset + length == oldoffset + oldlen; + ByteBuffer slice = Utils.sliceOrCopy(streamData, newpos, length); + return new StreamFrame(streamID, offset, length, fin, slice, false); + } + + /** + * {@return the stream id} + */ + public long streamId() { + return streamID; + } + + /** + * {@return whether this frame has a length} + * A frame that doesn't have a length must be the last + * frame in the packet. + */ + public boolean hasLength() { + return length != -1; + } + + /** + * {@return true if this is the last frame in the stream} + * The last frame has the FIN bit set. + */ + public boolean isLast() { return fin; } + + @Override + public long getTypeField() { + return STREAM | (hasLength() ? LEN : 0) + | (typeFieldHasOFF ? OFF : 0) + | (fin ? FIN : 0); + } + + @Override + public void encode(ByteBuffer dest) { + if (size() > dest.remaining()) { + throw new BufferOverflowException(); + } + int pos = dest.position(); + encodeVLField(dest, getTypeField(), "type"); + encodeVLField(dest, streamID, "streamID"); + if (typeFieldHasOFF) { + encodeVLField(dest, offset, "offset"); + } + if (hasLength()) { + encodeVLField(dest, length, "length"); + assert streamData.remaining() == length; + } + putByteBuffer(dest, streamData); + assert dest.position() - pos == size(); + } + + @Override + public int size() { + int size = getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(streamID); + if (typeFieldHasOFF) { + size += getVLFieldLengthFor(offset); + } + if (hasLength()) { + return size + getVLFieldLengthFor(length) + length; + } else { + return size + streamData.remaining(); + } + } + + /** + * {@return the frame payload} + */ + public ByteBuffer payload() { + return streamData.slice(); + } + + /** + * {@return the frame offset} + */ + public long offset() { return offset; } + + /** + * {@return the number of data bytes in the frame} + * @apiNote + * This is equivalent to calling {@code payload().remaining()}. + */ + public int dataLength() { + return dataLength; + } + + public static int compareOffsets(StreamFrame sf1, StreamFrame sf2) { + return Long.compare(sf1.offset, sf2.offset); + } + + /** + * Computes the header size that would be required to encode a frame with + * the given streamId, offset, and length. + * @apiNote + * This method is useful to figure out how many bytes can be allocated for + * the frame data, given a size constraint imposed by the space available + * for the whole datagram payload. + * @param encoder the {@code QuicPacketEncoder} - which can be used in case + * some part of the computation is Quic-version dependent. + * @param streamId the stream id + * @param offset the stream offset + * @param length the estimated length of the frame, typically this will be + * the min between the data available in the stream with respect + * to flow control, and the maximum remaining size for the datagram + * payload + * @return the estimated size of the header for a {@code StreamFrame} that would + * be created with the given parameters. + */ + public static int headerSize(QuicPacketEncoder encoder, long streamId, long offset, long length) { + // the header length is the size needed to encode the frame type, + // plus the size needed to encode the streamId, plus the size needed + // to encode the offset (if not 0) and the size needed to encode the + // length (if present) + int headerLength = getVLFieldLengthFor(STREAM | OFF | LEN | FIN) + + getVLFieldLengthFor(streamId); + if (offset != 0) headerLength += getVLFieldLengthFor(offset); + if (length >= 0) headerLength += getVLFieldLengthFor(length); + return headerLength; + } + + @Override + public String toString() { + return "StreamFrame(stream=" + streamID + + ", offset=" + offset + + ", length=" + length + + ", fin=" + fin + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java new file mode 100644 index 00000000000..69292ffbbc0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STREAMS_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamsBlockedFrame extends QuicFrame { + + private final long maxStreams; + private final boolean bidi; + + /** + * Incoming STREAMS_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + StreamsBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAMS_BLOCKED); + bidi = (type == STREAMS_BLOCKED); + maxStreams = decodeVLField(buffer, "maxStreams"); + if (maxStreams > MaxStreamsFrame.MAX_VALUE) { + throw new QuicTransportException("Invalid maximum streams", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * Outgoing STREAMS_BLOCKED frame + */ + public StreamsBlockedFrame(boolean bidi, long maxStreams) { + super(STREAMS_BLOCKED); + this.bidi = bidi; + this.maxStreams = requireVLRange(maxStreams, "maxStreams"); + } + + @Override + public long getTypeField() { + return STREAMS_BLOCKED + (bidi?0:1); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, maxStreams, "maxStreams"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(STREAMS_BLOCKED) + + getVLFieldLengthFor(maxStreams); + } + + /** + */ + public long maxStreams() { + return maxStreams; + } + + public boolean isBidi() { + return bidi; + } + + @Override + public String toString() { + return "StreamsBlockedFrame(bidi=" + bidi + + ", maxStreams=" + maxStreams + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java new file mode 100644 index 00000000000..dcdd040c0ed --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +/** + *

    Internal classes for the Quic protocol implementation

    + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java new file mode 100644 index 00000000000..a037f2e9387 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic Handshake Packets, as defined by + * RFC 9000, Section 17.2.4: + * + *
    {@code
    + *    A Handshake packet uses long headers with a type value of 0x02, followed
    + *    by the Length and Packet Number fields; see Section 17.2. The first byte
    + *    contains the Reserved and Packet Number Length bits; see Section 17.2.
    + *    It is used to carry cryptographic handshake messages and acknowledgments
    + *    from the server and client.
    + *
    + *    Handshake Packet {
    + *      Header Form (1) = 1,
    + *      Fixed Bit (1) = 1,
    + *      Long Packet Type (2) = 2,
    + *      Reserved Bits (2),
    + *      Packet Number Length (2),
    + *      Version (32),
    + *      Destination Connection ID Length (8),
    + *      Destination Connection ID (0..160),
    + *      Source Connection ID Length (8),
    + *      Source Connection ID (0..160),
    + *      Length (i),
    + *      Packet Number (8..32),
    + *      Packet Payload (..),
    + *    }
    + * }
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Handshake Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see + * RFC 9000, Section 17.2 + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface HandshakePacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.HANDSHAKE; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.HANDSHAKE; + } + + @Override + default boolean hasLength() { return true; } + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java new file mode 100644 index 00000000000..7be864c8b9c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic Initial Packets, as defined by + * RFC 9000, Section 17.2.2: + * + *

    {@code
    + *    An Initial packet uses long headers with a type value of 0x00.
    + *    It carries the first CRYPTO frames sent by the client and server to perform
    + *    key exchange, and it carries ACK frames in either direction.
    + *
    + *    Initial Packet {
    + *      Header Form (1) = 1,
    + *      Fixed Bit (1) = 1,
    + *      Long Packet Type (2) = 0,
    + *      Reserved Bits (2),         # Protected
    + *      Packet Number Length (2),  # Protected
    + *      Version (32),
    + *      DCID Len (8),
    + *      Destination Connection ID (0..160),
    + *      SCID Len (8),
    + *      Source Connection ID (0..160),
    + *      Token Length (i),
    + *      Token (..),
    + *      Length (i),
    + *      Packet Number (8..32),     # Protected
    + *      # Protected Packet Payload (..)
    + *      Protected Payload (0..24), # Skipped Part
    + *      Protected Payload (128),   # Sampled Part
    + *      Protected Payload (..)     # Remainder
    + *    }
    + * }
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Initial Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface InitialPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.INITIAL; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.INITIAL; + } + + @Override + default boolean hasLength() { return true; } + + /** + * {@return the length of the token field, if present, 0 if not} + */ + int tokenLength(); + + /** + * {@return the token bytes, if present, {@code null} if not} + * + * From + * RFC 9000, Section 17.2.2: + * + *

    {@code
    +     *    The value of the token that was previously provided
    +     *    in a Retry packet or NEW_TOKEN frame; see Section 8.1.
    +     * }
    + * + * @see + * RFC 9000, Section 8.1 + */ + byte[] token(); + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); + + @Override + default String prettyPrint() { + return String.format("%s(pn:%s, size=%s, token[%s], frames:%s)", packetType(), packetNumber(), + size(), tokenLength(), frames()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java new file mode 100644 index 00000000000..9c9a6e6ef31 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.quic.QuicConnectionId; + +/** + * This class models Quic Long Header Packet header, as defined by + * RFC 8999, Section 5.1: + * + *
    {@code
    + *    Long Header Packet {
    + *       Header Form (1) = 1,
    + *       Version-Specific Bits (7),
    + *       Version (32),
    + *       Destination Connection ID Length (8),
    + *       Destination Connection ID (0..2040),
    + *       Source Connection ID Length (8),
    + *       Source Connection ID (0..2040),
    + *       Version-Specific Data (..),
    + *    }
    + * }
    + * + * @param version version + * @param destinationId Destination Connection ID + * @param sourceId Source Connection ID + * @param headerLength length in bytes of the packet header + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + */ +public record LongHeader(int version, + QuicConnectionId destinationId, + QuicConnectionId sourceId, + int headerLength) { +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java new file mode 100644 index 00000000000..960aef6530b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.quic.QuicConnectionId; + +/** + * This class models Quic Long Header Packets, as defined by + * RFC 8999, Section 5.1: + * + *
    {@code
    + *    Long Header Packet {
    + *       Header Form (1) = 1,
    + *       Version-Specific Bits (7),
    + *       Version (32),
    + *       Destination Connection ID Length (8),
    + *       Destination Connection ID (0..2040),
    + *       Source Connection ID Length (8),
    + *       Source Connection ID (0..2040),
    + *       Version-Specific Data (..),
    + *    }
    + * }
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface LongHeaderPacket extends QuicPacket { + @Override + default HeadersType headersType() { return HeadersType.LONG; } + + /** + * {@return the packet's source connection ID} + */ + QuicConnectionId sourceId(); + + /** + * {@return the Quic version of the packet} + */ + int version(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java new file mode 100644 index 00000000000..7df9f904a82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic 1-RTT packets, as defined by + * RFC 9000, Section 17.3.1: + * + *

    {@code
    + *    A 1-RTT packet uses a short packet header. It is used after the
    + *    version and 1-RTT keys are negotiated.
    + *
    + *    1-RTT Packet {
    + *      Header Form (1) = 0,
    + *      Fixed Bit (1) = 1,
    + *      Spin Bit (1),
    + *      Reserved Bits (2),         # Protected
    + *      Key Phase (1),             # Protected
    + *      Packet Number Length (2),  # Protected
    + *      Destination Connection ID (0..160),
    + *      Packet Number (8..32),     # Protected
    + *      # Protected Packet Payload:
    + *      Protected Payload (0..24), # Skipped Part
    + *      Protected Payload (128),   # Sampled Part
    + *      Protected Payload (..),    # Remainder
    + *    }
    + * }
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Quic Version 2 uses the same 1-RTT packet structure than + * Quic Version 1. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface OneRttPacket extends ShortHeaderPacket { + + @Override + List frames(); + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.APPLICATION; + } + + @Override + default PacketType packetType() { + return PacketType.ONERTT; + } + + /** + * Returns the packet's Key Phase Bit: 0 or 1, if known. + * Returns -1 for outgoing packets. + * RFC 9000, Section 17.3.1: + * + *

    {@code
    +     *     Bit (0x04) of byte 0 indicates the key phase, which allows a recipient
    +     *     of a packet to identify the packet protection keys that are used to
    +     *     protect the packet. See [QUIC-TLS] for details.
    +     *     This bit is protected using header protection; see Section 5.4 of [QUIC-TLS].
    +     * }
    + * + * @return the packet's Key Phase Bit + * + * @see RFC 9001, [QUIC-TLS] + * @see RFC 9001, Section 5.4, [QUIC-TLS] + */ + default int keyPhase() { + return -1; + } + + /** + * Returns the packet's Latency Spin Bit: 0 or 1, if known. + * Returns -1 for outgoing packets. + * RFC 9000, Section 17.3.1: + * + *
    {@code
    +     *     The third most significant bit (0x20) of byte 0 is the latency spin
    +     *     bit, set as described in Section 17.4.
    +     * }
    + * + * @return the packet's Latency Spin Bit + * + * @see RFC 9000, Section 17.4 + */ + default int spin() { + return -1; + } + + @Override + default String prettyPrint() { + return String.format("%s(pn:%s, size=%s, phase:%s, spin:%s, frames:%s)", packetType(), packetNumber(), + size(), keyPhase(), spin(), frames()); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java new file mode 100644 index 00000000000..cea0854e0e5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.http.quic.PacketSpaceManager; +import jdk.internal.net.quic.QuicTransportException; + +/** + * An interface implemented by classes which keep track of packet + * numbers for a given packet number space. + */ +public sealed interface PacketSpace permits PacketSpaceManager { + + /** + * called on application packet space to record peer's transport parameters + * @param peerDelay max_ack_delay + * @param ackDelayExponent ack_delay_exponent + */ + void updatePeerTransportParameters(long peerDelay, long ackDelayExponent); + + /** + * {@return the packet number space managed by this class} + */ + PacketNumberSpace packetNumberSpace(); + + /** + * The largest processed PN is used to compute + * the packet number of an incoming Quic packet. + * + * @return the largest incoming packet number that + * was successfully processed in this space. + */ + long getLargestProcessedPN(); + + /** + * The largest received acked PN is used to compute the + * packet number that we include in an outgoing Quic packet. + * + * @return the largest packet number that was acknowledged by + * the peer in this space. + */ + long getLargestPeerAckedPN(); + + /** + * {@return the largest packet number that we have acknowledged in this + * space} + * + * @apiNote This is necessarily greater or equal to the packet number + * returned by {@linkplain #getMinPNThreshold()}. + */ + long getLargestSentAckedPN(); + + /** + * {@return the packet number threshold below which packets should be + * discarded without being processed in this space} + * + * @apiNote + * This corresponds to the largest acknowledged packet number + * carried in an outgoing ACK frame whose packet number has + * been acknowledged by the peer. In other words, the largest + * packet number sent by the peer for which we know that the + * peer has received an acknowledgement. + *

    + * Note that we need to track the ACK of outgoing packets that + * contain ACK frames in order to figure out whether a peer + * knows that a particular packet number has been received and + * avoid retransmission. However - we don't want ACK frames to grow + * too big and therefore we can drop some of the information, + * based on the largestSentAckedPN - see RFC 9000 Section 13.2 + * + */ + long getMinPNThreshold(); + + /** + * {@return a new packet number atomically allocated in this space} + */ + long allocateNextPN(); + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful negotiation of a new version. + * In that case we should stop retransmitting packet that have the + * "wrong" version: they will never be acknowledged. + */ + void versionChanged(); + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful processing of retry packet. + * In that case we should treat all previously sent packets as lost. + */ + void retry(); + + /** + * {@return a lock used by the transmission task}. + * Used to ensure that the transmission task does not observe partial changes + * during processing of incoming Versions and Retry packets. + */ + ReentrantLock getTransmitLock(); + /** + * Called when a packet is received. Causes the next ack frame to be + * updated. If a packet contains an {@link AckFrame}, the caller is + * expected to also later call {@link #processAckFrame(AckFrame)} + * when processing the packet payload. + * + * @param packet the received packet + * @param packetNumber the received packet number + * @param isAckEliciting whether this packet is ack eliciting + */ + void packetReceived(PacketType packet, long packetNumber, boolean isAckEliciting); + + /** + * Signals that a packet has been sent. + * This method is called by {@link QuicConnectionImpl} when a packet has been + * pushed to the endpoint for sending. + *

    The retransmitted packet is taken out the pendingRetransmission list and + * the new packet is inserted in the pendingAcknowledgement list. + * + * @param packet the new packet being retransmitted + * @param previousPacketNumber the packet number of the previous packet that was not acknowledged, + * or -1 if this is not a retransmission + * @param packetNumber the new packet number under which this packet is being retransmitted + * @throws IllegalArgumentException If {@code newPacketNumber} is lesser than 0 + */ + void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber); + + /** + * Processes a received ACK frame. + * This method is called by {@link QuicConnectionImpl}. + * + * @param frame the ACK frame received. + */ + void processAckFrame(AckFrame frame) throws QuicTransportException; + + /** + * Signals that the peer confirmed the handshake. Application space only. + */ + void confirmHandshake(); + + /** + * Get the next ack frame to send. + * This method returns the prepared ack frame if: + * - it was not sent yet + * - there are new ack-eliciting packets to acknowledge + * - optionally, if the ack frame is overdue + * + * @param onlyOverdue if true, the frame will only be returned if it's overdue + * @return The next AckFrame to send to the peer, or {@code null} + * if there is nothing to acknowledge. + */ + AckFrame getNextAckFrame(boolean onlyOverdue); + + /** + * Get the next ack frame to send. + * This method returns the prepared ack frame if: + * - it was not sent yet + * - there are new ack-eliciting packets to acknowledge + * - the ack frame size doesn't exceed {@code maxSize} + * - optionally, if the ack frame is overdue + * + * @param onlyOverdue if true, the frame will only be returned if it's overdue + * @param maxSize + * @return The next AckFrame to send to the peer, or {@code null} + * if there is nothing to acknowledge. + */ + AckFrame getNextAckFrame(boolean onlyOverdue, int maxSize); + + /** + * Used to request sending of a ping frame, for instance, to verify that + * the connection is alive. + * @return a completable future that will be completed with the time it + * took, in milliseconds, for the peer to acknowledge the packet that + * contained the PingFrame (or any packet that was sent after) + * + * @apiNote The returned completable future is actually completed + * if any packet whose packet number is greater than the packet number + * that contained the ping frame is acknowledged. + */ + CompletableFuture requestSendPing(); + + /** + * Stops retransmission for this packet space. + */ + void close(); + + /** + * {@return true if this packet space is closed} + */ + boolean isClosed(); + + /** + * Triggers immediate run of transmit loop. + * + * This method is called by {@link QuicConnectionImpl} when new data may be + * available for sending, for example: + * - new stream data is available + * - new receive credit is available + * - stream is forcibly closed + */ + void runTransmitter(); + + /** + * {@return true if a packet with that packet number + * is already being acknowledged (will be, or has been + * acknowledged)} + * @param packetNumber the packet number + */ + boolean isAcknowledged(long packetNumber); + + /** + * Immediately retransmit one unacknowledged initial packet + * @spec https://www.rfc-editor.org/rfc/rfc9002#name-speeding-up-handshake-compl + * RFC 9002 6.2.3. Speeding up Handshake Completion + */ + void fastRetransmit(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java new file mode 100644 index 00000000000..fa04cb3c947 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; +import java.util.Optional; + +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; + +/** + * A super-interface for all specific Quic packet implementation + * classes. + */ +public interface QuicPacket { + + /** + * {@return the packet's Destination Connection ID} + * + * @see + * RFC 9000, Section 7.2 + */ + QuicConnectionId destinationId(); + + /** + * The packet number space. + * NONE is for packets that don't have a packet number, + * such as Stateless Reset. + */ + enum PacketNumberSpace { + INITIAL, HANDSHAKE, APPLICATION, NONE; + + /** + * Maps a {@code PacketType} to the corresponding + * packet number space. + *

    + * For {@link PacketType#RETRY}, {@link PacketType#VERSIONS}, and + * {@link PacketType#NONE}, {@link PacketNumberSpace#NONE} is returned. + * + * @param packetType a packet type + * + * @return the packet number space that corresponds to the + * given packet type + */ + public static PacketNumberSpace of(PacketType packetType) { + return switch (packetType) { + case ONERTT, ZERORTT -> APPLICATION; + case INITIAL -> INITIAL; + case HANDSHAKE -> HANDSHAKE; + case RETRY, VERSIONS, NONE -> NONE; + }; + } + + /** + * Maps a {@code KeySpace} to the corresponding + * packet number space. + *

    + * For {@link KeySpace#RETRY}, {@link PacketNumberSpace#NONE} + * is returned. + * + * @param keySpace a key space + * + * @return the packet number space that corresponds to the given + * key space. + */ + public static PacketNumberSpace of(KeySpace keySpace) { + return switch (keySpace) { + case ONE_RTT, ZERO_RTT -> APPLICATION; + case HANDSHAKE -> HANDSHAKE; + case INITIAL -> INITIAL; + case RETRY -> NONE; + }; + } + } + + /** + * The packet type for Quic packets. + */ + enum PacketType { + NONE, INITIAL, VERSIONS, ZERORTT, HANDSHAKE, RETRY, ONERTT; + public boolean isLongHeaderType() { + return switch (this) { + case ONERTT, NONE, VERSIONS -> false; + default -> true; + }; + } + + /** + * {@return true if packets of this type are short-header packets} + */ + public boolean isShortHeaderType() { + return this == ONERTT; + } + + /** + * {@return the QUIC-TLS key space corresponding to this packet type} + * Some packet types, such as {@link #VERSIONS}, do not have an associated + * key space. + */ + public Optional keySpace() { + return switch (this) { + case INITIAL -> Optional.of(KeySpace.INITIAL); + case HANDSHAKE -> Optional.of(KeySpace.HANDSHAKE); + case RETRY -> Optional.of(KeySpace.RETRY); + case ZERORTT -> Optional.of(KeySpace.ZERO_RTT); + case ONERTT -> Optional.of(KeySpace.ONE_RTT); + case VERSIONS -> Optional.empty(); + case NONE -> Optional.empty(); + }; + } + } + + /** + * The Headers Type of the packet. + * This is either SHORT or LONG, or NONE when it can't be + * determined, or when we know that the packet is a stateless + * reset packet. A stateless reset packet is indistinguishable + * from a short header packet, so we only know that a packet + * is a stateless reset if we built it. In that case, the packet + * may advertise its header's type as NONE. + */ + enum HeadersType { NONE, SHORT, LONG} + + /** + * {@return this packet's number space} + */ + PacketNumberSpace numberSpace(); + + /** + * This packet size. + * @return the number of bytes needed to encode the packet. + * @see #payloadSize() + * @see #length() + */ + int size(); + + /** + * {@return true if this packet is ACK-eliciting} + * A packet is ACK-eliciting if it contains any + * {@linkplain QuicFrame#isAckEliciting() + * ACK-eliciting frame}. + */ + default boolean isAckEliciting() { + List frames = frames(); + if (frames == null || frames.isEmpty()) return false; + return frames.stream().anyMatch(QuicFrame::isAckEliciting); + } + + /** + * Whether this packet has a length field whose value can be read + * from the packet bytes. + * @return whether this packet has a length. + */ + default boolean hasLength() { + return switch (packetType()) { + case INITIAL, ZERORTT, HANDSHAKE -> true; + default -> false; + }; + } + + /** + * Returns the length of the payload and packet number. Includes encryption tag. + * + * This is the value stored in the {@code Length} field in Initial, + * Handshake and 0-RTT packets. + * @return the length of the payload and packet number. + * @throws UnsupportedOperationException if this packet type does not have + * the {@code Length} field. + * @see #hasLength() + * @see #size() + * @see #payloadSize() + */ + default int length() { + throw new UnsupportedOperationException(); + } + + /** + * This packet header's type. Either SHORT or LONG. + * @return this packet's header's type. + */ + HeadersType headersType(); + + /** + * {@return this packet's type} + */ + PacketType packetType(); + + /** + * {@return this packet's packet number, if applicable, {@code -1L} otherwise} + */ + default long packetNumber() { + return -1L; + } + + /** + * {@return this packet's frames} + */ + default List frames() { + return List.of(); + } + + /** + * {@return the packet's payload size} + * This is the number of bytes needed to encode the packet's + * {@linkplain #frames() frames}. + * @see #size() + * @see #length() + */ + default int payloadSize() { + List frames = frames(); + if (frames == null || frames.isEmpty()) return 0; + return frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + } + + default String prettyPrint() { + long pn = packetNumber(); + if (pn >= 0) { + return String.format("%s(pn:%s, size=%s, frames:%s)", packetType(), pn, size(), frames()); + } else { + return String.format("%s(size=%s)", packetType(), size()); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java new file mode 100644 index 00000000000..233a1a35778 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java @@ -0,0 +1,1748 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import javax.crypto.AEADBadTagException; +import javax.crypto.ShortBufferException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.Objects; +import java.util.List; +import java.nio.BufferUnderflowException; + +/** + * A {@code QuicPacketDecoder} encapsulates the logic to decode a + * quic packet. A {@code QuicPacketDecoder} is typically tied to + * a particular version of the QUIC protocol. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public class QuicPacketDecoder { + + private static final Logger debug = Utils.getDebugLogger(() -> "QuicPacketDecoder"); + + private final QuicVersion quicVersion; + private QuicPacketDecoder(final QuicVersion quicVersion) { + this.quicVersion = quicVersion; + } + + /** + * Reads the headers type from the given byte. + * @param first the first byte of a quic packet + * @return the headers type encoded in the given byte. + */ + private static QuicPacket.HeadersType headersType(byte first) { + int type = first & 0x80; + return type == 0 ? QuicPacket.HeadersType.SHORT : QuicPacket.HeadersType.LONG; + } + + /** + * Peeks at the headers type in the given byte buffer. + * Does not advance the cursor. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit.The provided offset must be less than the buffer + * limit in order for this method to read the header + * bytes. + * + * @param buffer the byte buffer containing a packet. + * @param offset the offset at which the packet starts. + * + * @return the header's type of the packet contained in this + * byte buffer. NONE if the header's type cannot be determined. + */ + public static QuicPacket.HeadersType peekHeaderType(ByteBuffer buffer, int offset) { + if (offset < 0 || offset >= buffer.limit()) return QuicPacket.HeadersType.NONE; + return headersType(buffer.get(offset)); + } + + /** + * Reads a connection ID length from the connection ID length + * byte. + * @param length the connection ID length byte. + * @return the connection ID length + */ + private static int connectionIdLength(byte length) { + // length is represented by an unsigned byte. + return length & 0xFF; + } + + /** + * Peeks at the connection id in the long header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the long header packet. + * + * @param buffer the buffer containing a long headers packet. + * @return A ByteBuffer slice containing the connection id bytes, + * or null if the packet is malformed and the connection id + * could not be read. + */ + public static ByteBuffer peekLongConnectionId(ByteBuffer buffer) { + // the connection id length starts at index 5 (1 byte for headers, + // 4 bytes for version) + var pos = buffer.position(); + var remaining = buffer.remaining(); + if (remaining < 6) return null; + int length = connectionIdLength(buffer.get(pos + 5)); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (length > remaining - 6) return null; + return buffer.slice(pos + 6, length); + } + + /** + * Peeks at the header in the long header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the long header packet. + * + * @param buffer the buffer containing a long header packet. + * @return A LongHeader containing the packet header data, + * or null if the packet is malformed + */ + public static LongHeader peekLongHeader(ByteBuffer buffer) { + return peekLongHeader(buffer, buffer.position()); + } + + /** + * Peeks at the header in the long header packet bytes. + * This method doesn't advance the cursor. + * + * @param buffer the buffer containing a long header packet. + * @param offset the position of the start of the packet + * @return A LongHeader containing the packet header data, + * or null if the packet is malformed + */ + public static LongHeader peekLongHeader(ByteBuffer buffer, int offset) { + // the destination connection id length starts at index 5 + // (1 byte for headers, 4 bytes for version) + // Therefore the packet needs at least 6 bytes to contain + // a DCID length (coded on 1 byte) + var remaining = buffer.remaining(); + var limit = buffer.limit(); + if (remaining < 7) return null; + if ((buffer.get(offset) & 0x80) == 0) { + // short header + return null; + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int version = buffer.getInt(offset+1); + + + // read the DCID length (coded on 1 byte) + int length = connectionIdLength(buffer.get(offset + 5)); + if (length < 0 || length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + QuicConnectionId destinationId = new PeerConnectionId(buffer.slice(offset + 6, length), null); + + // We need at least 6 + length + 1 byte to have + // a chance to read the SCID length (coded on 1 byte) + if (length > remaining - 7) return null; + int srcPos = offset + 6 + length; + + // read the SCID length + int srclength = connectionIdLength(buffer.get(srcPos)); + if (srclength < 0 || srclength > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + // we need at least pos + srclength + 1 byte in the + // packet to peek at the SCID + if (srclength > limit - srcPos - 1) return null; + QuicConnectionId sourceId = new PeerConnectionId(buffer.slice(srcPos + 1, srclength), null); + int headerLength = 7 + length + srclength; + + // Return the SCID as a buffer slice. + // The SCID begins at pos + 1 and has srclength bytes. + return new LongHeader(version, destinationId, sourceId, headerLength); + } + + /** + * Returns a bytebuffer containing the token from initial packet. + * This method doesn't advance the cursor. + * The buffer position must be at the start of an initial packet. + * @apiNote + * If the initial packet doesn't contain any token, an empty + * {@code ByteBuffer} is returned. + * @param buffer the buffer containing an initial packet. + * @return token or null if packet is malformed + */ + public static ByteBuffer peekInitialPacketToken(ByteBuffer buffer) { + + // the destination connection id length starts at index 5 + // (1 byte for headers, 4 bytes for version) + // Therefore the packet needs at least 6 bytes to contain + // a DCID length (coded on 1 byte) + var pos = buffer.position(); + var remaining = buffer.remaining(); + var limit = buffer.limit(); + if (remaining < 6) return null; + + // read the DCID length (coded on 1 byte) + int length = connectionIdLength(buffer.get(pos + 5)); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (length < 0) return null; + + // skip the DCID, and read the SCID length + // We need at least 6 + length + 1 byte to have + // a chance to read the SCID length (coded on 1 byte) + pos = pos + 6 + length; + if (pos > limit - 1) return null; + + // read the SCID length + int srclength = connectionIdLength(buffer.get(pos)); + if (srclength > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (srclength < 0) return null; + // we need at least pos + srclength + 1 byte in the + // packet to peek at the token + if (srclength > limit - pos - 1) return null; + + //skip the SCID, and read the token length + pos = pos + srclength + 1; + + // read the token length + int tokenLengthLength = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + assert tokenLengthLength <= 8; + if (pos > limit - tokenLengthLength -1) return null; + long tokenLength = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (tokenLength < 0 || tokenLength > Integer.MAX_VALUE) return null; + if (tokenLength > limit - pos - tokenLengthLength) return null; + + // return the token + return buffer.slice(pos + tokenLengthLength, (int)tokenLength); + } + + /** + * Peeks at the connection id in the short header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the short header packet. + * + * @param buffer the buffer containing a short headers packet. + * @param length the connection id length. + * + * @return A ByteBuffer slice containing the connection id bytes, + * or null if the packet is malformed and the connection id + * could not be read. + */ + public static ByteBuffer peekShortConnectionId(ByteBuffer buffer, int length) { + int pos = buffer.position(); + int limit = buffer.limit(); + assert pos >= 0; + assert length <= QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + if (limit - pos < length + 1) return null; + return buffer.slice(pos+1, length); + } + + /** + * Returns the version of the first packet in the buffer. + * This method doesn't advance the cursor. + * Returns 0 if the version is 0 (version negotiation packet), + * or if the version cannot be determined. + * The packet is expected to start at the buffer's current position. + * + * @implNote + * This is equivalent to calling: + * {@code peekVersion(buffer, buffer.position())}. + * + * @param buffer the buffer containing the packet. + * + * @return the version of the packet in the buffer, or 0. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + public static int peekVersion(ByteBuffer buffer) { + return peekVersion(buffer, buffer.position()); + } + + /** + * Returns the version of the first packet in the buffer. + * This method doesn't advance the cursor. + * Returns 0 if the version is 0 (version negotiation packet), + * or if the version cannot be determined. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit. The buffer limit must allow for reading + * the header byte and version number starting at the offset. + * + * @param buffer the buffer containing the packet. + * @param offset the offset at which the packet starts. + * + * @return the version of the packet in the buffer, or 0. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + public static int peekVersion(ByteBuffer buffer, int offset) { + int limit = buffer.limit(); + assert offset >= 0; + if (limit - offset < 5) return 0; + QuicPacket.HeadersType headersType = peekHeaderType(buffer, offset); + if (headersType == QuicPacket.HeadersType.LONG) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.getInt(offset+1); + } + return 0; + } + + /** + * Returns true if the first packet in the buffer is a version + * negotiation packet. + * This method doesn't advance the cursor. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit. If the packet is a long header packet, + * the buffer limit must allow for reading + * the header byte and version number starting at the offset. + * + * @param buffer the buffer containing the packet. + * @param offset the offset at which the packet starts. + * + * @return true if the first packet in the buffer is a version + * negotiation packet. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + private static boolean isVersionNegotiation(ByteBuffer buffer, int offset) { + int limit = buffer.limit(); + if (limit - offset < 5) return false; + QuicPacket.HeadersType headersType = peekHeaderType(buffer, offset); + if (headersType == QuicPacket.HeadersType.LONG) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.getInt(offset+1) == 0; + } + return false; + } + + public abstract static class IncomingQuicPacket implements QuicPacket { + private final QuicConnectionId destinationId; + + protected IncomingQuicPacket(QuicConnectionId destinationId) { + this.destinationId = destinationId; + } + + @Override + public final QuicConnectionId destinationId() { return destinationId; } + } + + private abstract static class IncomingLongHeaderPacket + extends IncomingQuicPacket implements LongHeaderPacket { + + private final QuicConnectionId sourceId; + private final int version; + IncomingLongHeaderPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version) { + super(destinationId); + this.sourceId = sourceId; + this.version = version; + } + + @Override + public final QuicConnectionId sourceId() { return sourceId; } + + @Override + public final int version() { return version; } + } + + private abstract static class IncomingShortHeaderPacket + extends IncomingQuicPacket implements ShortHeaderPacket { + + IncomingShortHeaderPacket(QuicConnectionId destinationId) { + super(destinationId); + } + } + + private static final class IncomingRetryPacket + extends IncomingLongHeaderPacket implements RetryPacket { + final int size; + final byte[] retryToken; + + private IncomingRetryPacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int size, byte[] retryToken) { + super(sourceId, destinationId, version); + this.size = size; + this.retryToken = retryToken; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] retryToken() { + return retryToken; + } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingRetryPacket}. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingRetryPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + */ + static IncomingRetryPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicTransportException { + try { + reader.verifyRetry(); + } catch (AEADBadTagException e) { + throw new IOException("Bad integrity tag", e); + } + + int size = reader.remaining(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Retry Token + byte[] retryToken = reader.readRetryToken(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(retryToken(%d), %s)", + retryToken.length, reader); + } + + // Retry Integrity Tag + assert reader.remaining() == 16; + byte[] retryIntegrityTag = reader.readRetryIntegrityTag(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(retryIntegrityTag(%d), %s)", + retryIntegrityTag.length, reader); + } + assert size == reader.bytesRead(); + + return new IncomingRetryPacket(sourceID, destinationID, version, + size, retryToken); + } + } + + private static final class IncomingHandshakePacket + extends IncomingLongHeaderPacket implements HandshakePacket { + + final int size; + final int length; + final long packetNumber; + final List frames; + + IncomingHandshakePacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int length, long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingHandshakePacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingHandshakePacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingHandshakePacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingHandshakePacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get length of packet number and payload + var packetLength = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(length(%d), %s)", + packetLength, reader); + } + + // Remove protection before reading packet number + reader.unprotectLong(packetLength); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (packetLength - packetNumberLength); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt HANDSHAKE packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.HANDSHAKE, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + assert size == reader.position() - reader.offset(); + + assert packetLength == (int)packetLength; + return new IncomingHandshakePacket(sourceID, destinationID, + version, (int)packetLength, packetNumber, frames, size); + } + } + + private static final class IncomingZeroRttPacket + extends IncomingLongHeaderPacket implements ZeroRttPacket { + + final int size; + final int length; + final long packetNumber; + final List frames; + + IncomingZeroRttPacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int length, long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingZeroRttPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingZeroRttPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingZeroRttPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingZeroRttPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get length of packet number and payload + var length = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(length(%d), %s)", + length, reader); + } + + // Remove protection before reading packet number + reader.unprotectLong(length); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (length - packetNumberLength); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt ZERORTT packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.ZERO_RTT, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + + assert length == (int)length; + return new IncomingZeroRttPacket(sourceID, destinationID, + version, (int)length, packetNumber, frames, size); + } + } + + private static final class IncomingOneRttPacket + extends IncomingShortHeaderPacket implements OneRttPacket { + + final int size; + final long packetNumber; + final List frames; + final int keyPhase; + final int spin; + + IncomingOneRttPacket(QuicConnectionId destinationId, + long packetNumber, List frames, + int spin, int keyPhase, int size) { + super(destinationId); + this.keyPhase = keyPhase; + this.spin = spin; + this.size = size; + this.packetNumber = packetNumber; + this.frames = frames; + } + + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public int keyPhase() { + return keyPhase; + } + + @Override + public int spin() { + return spin; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingOneRttPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingOneRttPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingOneRttPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingOneRttPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(headers(%x), %s)", + headers, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readShortConnectionId(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + + // Remove protection before reading packet number + reader.unprotectShort(); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = reader.remaining(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + final int keyPhase = (headers & 0x04) >> 2; + // keyphase is a 1 bit structure, so only 0 or 1 are valid values + assert keyPhase == 0 || keyPhase == 1 : "unexpected key phase: " + keyPhase; + final int spin = (headers & 0x20) >> 5; + assert spin == 0 || spin == 1 : "unexpected spin bit: " + spin; + + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, keyPhase); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt ONERTT packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.3.1 + if ((headers & 0x18) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.ONE_RTT, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + + return new IncomingOneRttPacket(destinationID, packetNumber, frames, spin, keyPhase, size); + } + } + + private static final class IncomingInitialPacket + extends IncomingLongHeaderPacket implements InitialPacket { + + final int size; + final int length; + final int tokenLength; + final long packetNumber; + final byte[] token; + final List frames; + + IncomingInitialPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, int version, + int tokenLength, byte[] token, int length, + long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.tokenLength = tokenLength; + this.token = token; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int tokenLength() { return tokenLength; } + + @Override + public byte[] token() { return token; } + + @Override + public int length() { return length; } + + @Override + public long packetNumber() { return packetNumber; } + + @Override + public int size() { return size; } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingInitialPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingInitialPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingInitialPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingInitialPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingInitialPacket.decode([protected]headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get number of bytes needed to store the length of the token + var tokenLength = (int) reader.readTokenLength(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(token-length(%d), %s)", + tokenLength, reader); + } + var token = reader.readToken(tokenLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(token(%d), %s)", + token == null ? 0 : token.length, reader); + } + + // Get length of packet number and payload + var packetLength = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(packetLength(%d), %s)", + packetLength, reader); + } + assert packetLength == (int)packetLength; + if (packetLength > reader.remaining()) { + if (debug.on()) { + debug.log("IncomingInitialPacket rejected, invalid length(%d/%d), %s)", + packetLength, reader.remaining(), reader); + } + throw new BufferUnderflowException(); + } + + + // get the size (in bytes) of new packet + int size = reader.bytesRead() + (int)packetLength; + + if (!context.verifyToken(destinationID, token)) { + if (debug.on()) { + debug.log("IncomingInitialPacket rejected, invalid token(%s), %s)", + token == null ? "null" : HexFormat.of().formatHex(token), + reader); + } + return null; + } + + // Remove protection before reading packet number + reader.unprotectLong(packetLength); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + int packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (packetLength - packetNumberLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt INITIAL packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.INITIAL, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + assert size == reader.bytesRead() : size - reader.bytesRead(); + + return new IncomingInitialPacket(sourceID, destinationID, + version, tokenLength, token, (int)packetLength, packetNumber, frames, size); + } + + } + + private static final class IncomingVersionNegotiationPacket + extends IncomingLongHeaderPacket + implements VersionNegotiationPacket { + + final int size; + final int[] versions; + + IncomingVersionNegotiationPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, int[] versions, + int size) { + super(sourceId, destinationId, version); + this.size = size; + this.versions = Objects.requireNonNull(versions); + } + + @Override + public int size() { return size; } + + @Override + public List frames() { return List.of(); } + + @Override + public int payloadSize() { return versions.length << 2; } + + @Override + public int[] supportedVersions() { + return versions; + } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingVersionNegotiationPacket}. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingVersionNegotiationPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + */ + static IncomingVersionNegotiationPacket decode(PacketReader reader, CodingContext context) + throws IOException { + + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + // The long header bit should be set. We should ignore the other 7 bits + assert QuicPacketDecoder.headersType(headers) == HeadersType.LONG || (headers & 0x80) == 0x80; + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Calculate payload length and retrieve payload + final int payloadLen = reader.remaining(); + final int versionsCount = payloadLen >> 2; + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + int[] versions = reader.readSupportedVersions(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + assert !reader.hasRemaining() : "%s superfluous bytes in buffer" + .formatted(reader.remaining()); + + // sanity checks: + var msg = "Bad version negotiation packet"; + if (payloadLen != versionsCount << 2) { + throw new IOException("%s: %s bytes after %s versions" + .formatted(msg, payloadLen % 4, versionsCount)); + } + if (versionsCount == 0) { + throw new IOException("%s: no supported versions in packet" + .formatted(msg)); + } + + return new IncomingVersionNegotiationPacket(sourceID, destinationID, + version, versions, size); + } + } + + /** + * Decode the contents of the given {@code ByteBuffer} and, depending on the + * {@link PacketType}, return a {@link QuicPacket} with the corresponding type. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer as appropriate. + * + *

    If successful, an {@code IncomingQuicPacket} instance is returned. + * The position of the buffer is moved to the first byte following the last + * decoded byte. The buffer limit is unchanged. + * + *

    Otherwise, an exception is thrown. The position of the buffer is unspecified, + * but is usually set at the place where the error occurred. + * + * @apiNote If successful, and the limit was not reached, this method should be + * called again to decode the next packet contained in the buffer. Otherwise, if + * an exception occurs, the remaining bytes in the buffer should be dropped, since + * the position of the next packet in the buffer cannot be determined with + * certainty. + * + * @param buffer the buffer with the bytes to be decoded + * @param context the decoding context + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ + public IncomingQuicPacket decode(ByteBuffer buffer, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + Objects.requireNonNull(buffer); + + assert buffer.order() == ByteOrder.BIG_ENDIAN; + PacketType type = peekPacketType(buffer); + PacketReader packetReader = new PacketReader(buffer, context, type); + + QuicTLSEngine.KeySpace keySpace = type.keySpace().orElse(null); + if (keySpace != null && !context.getTLSEngine().keysAvailable(keySpace)) { + if (debug.on()) { + debug.log("QuicPacketDecoder.decode(%s): no keys, skipping", packetReader); + } + return null; + } + + return switch (type) { + case RETRY -> IncomingRetryPacket.decode(packetReader, context); + case ONERTT -> IncomingOneRttPacket.decode(packetReader, context); + case ZERORTT -> IncomingZeroRttPacket.decode(packetReader, context); + case HANDSHAKE -> IncomingHandshakePacket.decode(packetReader, context); + case INITIAL -> IncomingInitialPacket.decode(packetReader, context); + case VERSIONS -> IncomingVersionNegotiationPacket.decode(packetReader, context); + case NONE -> throw new IOException("Unknown type: " + type); // if junk received + default -> throw new IOException("Not implemented: " + type); // if has type but not recognised + }; + } + + private static QuicConnectionId decodeConnectionID(ByteBuffer buffer) { + if (!buffer.hasRemaining()) + throw new BufferUnderflowException(); + + int len = buffer.get() & 0xFF; + if (len > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] destinationConnectionID = new byte[len]; + + // Save buffer position ahead of time to check after read + int pos = buffer.position(); + buffer.get(destinationConnectionID); + // Ensure all bytes have been read correctly + assert pos + len == buffer.position(); + + return new PeerConnectionId(destinationConnectionID); + } + + /** + * Peek at the size of the first packet present in the buffer. + * The position of the buffer must be at the first byte of the + * first packet. This method doesn't advance the buffer position. + * @param buffer A byte buffer containing quic packets + * @return the size of the first packet present in the buffer. + */ + public int peekPacketSize(ByteBuffer buffer) { + int pos = buffer.position(); + int limit = buffer.limit(); + int available = limit - pos; + assert available >= 0 : available; + if (available <= 0) return available; + PacketType type = peekPacketType(buffer); + return switch (type) { + case HANDSHAKE, INITIAL, ZERORTT -> { + assert peekVersion(buffer, pos) == quicVersion.versionNumber(); + int end = peekPacketEnd(type, buffer); + assert end <= limit; + yield end - pos; + } + // ONERTT, RETRY, VERSIONS, NONE: + default -> available; + }; + } + + /** + * Reads the Quic V1 packet type from the given byte. + * + * @param headerByte the first byte of a quic packet + * @return the packet type encoded in the given byte. + */ + private PacketType packetType(byte headerByte) { + int htype = headerByte & 0xC0; + int ptype = headerByte & 0xF0; + return switch (htype) { + case 0xC0 -> switch (quicVersion) { + case QUIC_V1 -> switch (ptype) { + case 0xC0 -> PacketType.INITIAL; + case 0xD0 -> PacketType.ZERORTT; + case 0xE0 -> PacketType.HANDSHAKE; + case 0xF0 -> PacketType.RETRY; + default -> PacketType.NONE; + }; + case QUIC_V2 -> switch (ptype) { + case 0xD0 -> PacketType.INITIAL; + case 0xE0 -> PacketType.ZERORTT; + case 0xF0 -> PacketType.HANDSHAKE; + case 0xC0 -> PacketType.RETRY; + default -> PacketType.NONE; + }; + }; + case 0x40 -> PacketType.ONERTT; // may be a stateless reset too + default -> PacketType.NONE; + }; + } + + public PacketType peekPacketType(ByteBuffer buffer) { + int offset = buffer.position(); + return peekPacketType(buffer, offset); + } + + public PacketType peekPacketType(ByteBuffer buffer, int offset) { + if (offset < 0 || offset >= buffer.limit()) return PacketType.NONE; + var headers = buffer.get(offset); + var headersType = headersType(headers); + if (headersType == QuicPacket.HeadersType.LONG) { + if (isVersionNegotiation(buffer, offset)) { + return PacketType.VERSIONS; + } + var version = peekVersion(buffer, offset); + if (version != quicVersion.versionNumber()) { + return PacketType.NONE; + } + } + return packetType(headers); + } + + /** + * Returns the position just after the first packet present in the buffer. + * @param type the first packet type. Must be INITIAL, HANDSHAKE, or ZERORTT. + * @param buffer the byte buffer containing the packet + * @return the position just after the first packet present in the buffer. + */ + private int peekPacketEnd(PacketType type, ByteBuffer buffer) { + // Store initial position to calculate size of packet decoded + int initialPosition = buffer.position(); + int limit = buffer.limit(); + assert buffer.order() == ByteOrder.BIG_ENDIAN; + assert type == PacketType.HANDSHAKE + || type == PacketType.INITIAL + || type == PacketType.ZERORTT : type; + // This case should have been handled by the caller + assert buffer.getInt(initialPosition + 1) != 0 : "version is 0"; + + int pos = initialPosition; // header bits + pos = pos + 4; // version + pos = pos + 1; // dcid length + if (pos <= 0 || pos >= limit) return limit; + int dcidlen = buffer.get(pos) & 0xFF; + pos = pos + dcidlen + 1; // scid length + if (pos <= 0 || pos >= limit) return limit; + int scidlen = buffer.get(pos) & 0xFF; + pos = pos + scidlen + 1; // token length or packet length + if (pos <= 0 || pos >= limit) return limit; + + if (type == PacketType.INITIAL) { + int tksize = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + if (tksize <= 0 || tksize > 8) return limit; + if (limit - tksize < pos) return limit; + long tklen = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (tklen < 0 || tklen > limit - pos) return limit; + pos = pos + tksize + (int)tklen; // packet length + if (pos <= 0 || pos >= limit) return limit; + } + + int lensize = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + if (lensize <= 0 || lensize > 8) return limit; + long len = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (len < 0 || len > limit - pos) return limit; + pos = pos + lensize + (int)len; // end of packet + if (pos <= 0 || pos >= limit) return limit; + return pos; + } + + /** + * Find the length of the next packet in the buffer, and return + * the next packet bytes as a slice of the original packet. + * Advances the original buffer position to after the returned + * packet. + * @param buffer a buffer containing coalesced packets + * @param offset the offset at which the next packet starts + * @return the next packet. + */ + public ByteBuffer nextPacketSlice(ByteBuffer buffer, int offset) { + assert offset >= 0; + assert offset <= buffer.limit(); + int pos = buffer.position(); + int limit = buffer.limit(); + buffer.position(offset); + ByteBuffer next = null; + try { + int size = peekPacketSize(buffer); + if (debug.on()) { + debug.log("next packet bytes from %d (%d/%d)", + offset, size, buffer.remaining()); + } + next = buffer.slice(offset, size); + buffer.position(offset + size); + } catch (Throwable tt) { + if (debug.on()) { + debug.log("failed to peek packet size: " + tt, tt); + debug.log("dropping all remaining bytes (%d)", limit - pos); + } + buffer.position(limit); + next = buffer; + } + return next; + } + + /** + * Advance the bytebuffer position to the end of the packet + * @param buffer A byte buffer containing quic packets + * @param offset The offset at which the packet starts + */ + public void skipPacket(ByteBuffer buffer, int offset) { + assert offset >= 0; + assert offset <= buffer.limit(); + int pos = buffer.position(); + int limit = buffer.limit(); + buffer.position(offset); + try { + int size = peekPacketSize(buffer); + if (debug.on()) { + debug.log("dropping packet bytes from %d (%d/%d)", + offset, size, buffer.remaining()); + } + buffer.position(offset + size); + } catch (Throwable tt) { + if (debug.on()) { + debug.log("failed to peek packet size: " + tt, tt); + debug.log("dropping all remaining bytes (%d)", limit - pos); + } + buffer.position(limit); + } + } + + /** + * Returns a decoder for the given Quic version. + * Returns {@code null} if no decoder for that version exists. + * + * @param quicVersion the Quic protocol version number + * @return a decoder for the given Quic version or {@code null} + */ + public static QuicPacketDecoder of(QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> Decoders.QUIC_V1_DECODER; + case QUIC_V2 -> Decoders.QUIC_V2_DECODER; + default -> throw new IllegalArgumentException("No packet decoder for Quic version " + quicVersion); + }; + } + + /** + * Returns a {@code QuicPacketDecoder} to decode the packet + * starting at the specified offset in the buffer. + * This method will attempt to read the quic version in the + * packet in order to return the proper decoder. + * If the version is 0, then the decoder for Quic Version 1 + * is returned. + * + * @param buffer A buffer containing a Quic packet + * @param offset The offset at which the packet starts + * @return A {@code QuicPacketDecoder} instance to decode the + * packet starting at the given offset. + */ + public static QuicPacketDecoder of(ByteBuffer buffer, int offset) { + var version = peekVersion(buffer, offset); + final QuicVersion quicVersion = version == 0 ? QuicVersion.QUIC_V1 + : QuicVersion.of(version).orElse(null); + if (quicVersion == null) { + return null; + } + return of(quicVersion); + } + + /** + * A {@code PacketReader} to read a Quic packet. + * A {@code PacketReader} may have version specific code, and therefore + * has an implicit pointer to a {@code QuicPacketDecoder} instance. + *

    + * A {@code PacketReader} offers high level helper methods to read + * data (such as Connection IDs or Packet Numbers) from a Quic packet. + * It has however no or little knowledge of the actual packet structure. + * It is driven by the {@code decode} method of the appropriate + * {@code IncomingQuicPacket} type. + *

    + * A {@code PacketReader} is stateful: it encapsulates a {@code ByteBuffer} + * (or possibly a list of byte buffers - as a future enhancement) and + * advances the position on the buffer it is reading. + * + */ + class PacketReader { + private static final int PACKET_NUMBER_MASK = 0x03; + final ByteBuffer buffer; + final int offset; + final int initialLimit; + final CodingContext context; + final PacketType packetType; + + PacketReader(ByteBuffer buffer, CodingContext context) { + this(buffer, context, peekPacketType(buffer)); + } + + PacketReader(ByteBuffer buffer, CodingContext context, PacketType packetType) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int pos = buffer.position(); + int limit = buffer.limit(); + this.buffer = buffer; + this.offset = pos; + this.initialLimit = limit; + this.context = context; + this.packetType = packetType; + } + + public int offset() { + return offset; + } + + public int position() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public int bytesRead() { + return position() - offset; + } + + public void reset() { + buffer.position(offset); + buffer.limit(initialLimit); + } + + public byte headers() { + return buffer.get(offset); + } + + public void headers(byte headers) { + buffer.put(offset, headers); + } + + public PacketType packetType() { + return packetType; + } + + public int packetNumberLength() { + return (headers() & PACKET_NUMBER_MASK) + 1; + } + + public byte readHeaders() { + return buffer.get(); + } + + public int readVersion() { + return buffer.getInt(); + } + + public int[] readSupportedVersions() { + // Calculate payload length and retrieve payload + final int payloadLen = buffer.remaining(); + final int versionsCount = payloadLen >> 2; + + int[] versions = new int[versionsCount]; + for (int i=0 ; i= 0 && packetLength <= VariableLengthEncoder.MAX_ENCODED_INTEGER + : packetLength; + if (packetLength > remaining()) { + throw new BufferUnderflowException(); + } + return packetLength; + } + + public long readTokenLength() { + return readVariableLength(); + } + + public byte[] readToken(int tokenLength) { + // Check to ensure that tokenLength is within valid range + if (tokenLength < 0 || tokenLength > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] token = tokenLength > 0 ? new byte[tokenLength] : null; + if (tokenLength > 0) { + buffer.get(token); + } + return token; + } + + public long readVariableLength() { + return VariableLengthEncoder.decode(buffer); + } + + public void maskPacketNumber(int packetNumberLength, ByteBuffer mask) { + int pos = buffer.position(); + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(pos + i, (byte)(buffer.get(pos + i) ^ mask.get())); + } + } + + public long readPacketNumber(int packetNumberLength) { + var packetNumberSpace = PacketNumberSpace.of(packetType); + var largestProcessedPN = context.largestProcessedPN(packetNumberSpace); + return QuicPacketNumbers.decodePacketNumber(largestProcessedPN, buffer, packetNumberLength); + } + + public long readPacketNumber() { + return readPacketNumber(packetNumberLength()); + } + + private ByteBuffer peekPayloadSlice(int relativeOffset, int length) { + int payloadStart = buffer.position() + relativeOffset; + return buffer.slice(payloadStart, length); + } + + private ByteBuffer decryptPayload(long packetNumber, int payloadLen, int keyPhase) + throws AEADBadTagException, QuicKeyUnavailableException, QuicTransportException { + // Calculate payload length and retrieve payload + ByteBuffer output = buffer.slice(); + // output's position is on the first byte of encrypted data + output.mark(); + int payloadStart = buffer.position(); + buffer.position(offset); + buffer.limit(payloadStart + payloadLen); + // buffer's position and limit are set to the boundaries of the encrypted packet + try { + context.getTLSEngine().decryptPacket(packetType.keySpace().get(), packetNumber, keyPhase, + buffer, payloadStart - offset, output); + } catch (ShortBufferException e) { + throw new QuicTransportException(e.toString(), null, 0, + QuicTransportErrors.INTERNAL_ERROR); + } + // buffer's position and limit are both at end of the packet + output.limit(output.position()); + output.reset(); + // output's position and limit are set to the boundaries of decrypted frame data + buffer.limit(initialLimit); + return output; + } + + public List parsePayloadSlice(ByteBuffer payload) + throws QuicTransportException { + if (!payload.hasRemaining()) { + throw new QuicTransportException("Packet with no frames", + packetType().keySpace().get(), 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + try { + List frames = new ArrayList<>(); + while (payload.hasRemaining()) { + int start = payload.position(); + frames.add(QuicFrame.decode(payload)); + int end = payload.position(); + assert start < end : "bytes remaining at offset %s: %s" + .formatted(start, payload.remaining()); + } + return frames; + } catch (RuntimeException e) { + throw new QuicTransportException(e.getMessage(), + packetType().keySpace().get(), 0, QuicTransportErrors.INTERNAL_ERROR); + } + } + + byte[] readRetryToken() { + var tokenLength = buffer.limit() - buffer.position() - 16; + assert tokenLength > 0; + byte[] retryToken = new byte[tokenLength]; + buffer.get(retryToken); + return retryToken; + } + + byte[] readRetryIntegrityTag() { + // The 16 last bytes in the datagram payload + assert remaining() == 16; + byte[] retryIntegrityTag = new byte[16]; + buffer.get(retryIntegrityTag); + return retryIntegrityTag; + } + + public void verifyRetry() throws AEADBadTagException, QuicTransportException { + // assume the buffer position and limit are set to packet boundaries + QuicTLSEngine tlsEngine = context.getTLSEngine(); + tlsEngine.verifyRetryPacket(quicVersion, + context.originalServerConnId().asReadOnlyBuffer(), buffer.asReadOnlyBuffer()); + } + + public QuicConnectionId readLongConnectionId() { + return decodeConnectionID(buffer); + } + + public QuicConnectionId readShortConnectionId() { + if (!buffer.hasRemaining()) + throw new BufferUnderflowException(); + + // Retrieve connection ID length from endpoint via context + int len = context.connectionIdLength(); + if (len > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] destinationConnectionID = new byte[len]; + + // Save buffer position ahead of time to check after read + int pos = buffer.position(); + buffer.get(destinationConnectionID); + // Ensure all bytes have been read correctly + assert pos + len == buffer.position(); + + return new PeerConnectionId(destinationConnectionID); + } + + @Override + public String toString() { + return "PacketReader(offset=%s, pos=%s, remaining=%s)" + .formatted(offset, position(), remaining()); + } + + public void unprotectLong(long packetLength) + throws QuicKeyUnavailableException, QuicTransportException { + unprotect(packetLength, (byte) 0x0f); + } + + public void unprotectShort() + throws QuicKeyUnavailableException, QuicTransportException { + unprotect(buffer.remaining(), (byte) 0x1f); + } + + private void unprotect(long packetLength, byte headerMask) + throws QuicKeyUnavailableException, QuicTransportException { + QuicTLSEngine tlsEngine = context.getTLSEngine(); + int sampleSize = tlsEngine.getHeaderProtectionSampleSize(packetType.keySpace().get()); + if (packetLength > buffer.remaining() || packetLength < sampleSize + 4) { + throw new BufferUnderflowException(); + } + ByteBuffer sample = peekPayloadSlice(4, sampleSize); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(packetType.keySpace().get(), true, sample); + byte headers = headers(); + headers ^= (byte) (encryptedSample.get() & headerMask); + headers(headers); + maskPacketNumber(packetNumberLength(), encryptedSample); + } + } + + + private static final class Decoders { + static final QuicPacketDecoder QUIC_V1_DECODER = new QuicPacketDecoder(QuicVersion.QUIC_V1); + static final QuicPacketDecoder QUIC_V2_DECODER = new QuicPacketDecoder(QuicVersion.QUIC_V2); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java new file mode 100644 index 00000000000..890c1a63a35 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java @@ -0,0 +1,1746 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.IntFunction; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import javax.crypto.ShortBufferException; + +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.encodePacketNumber; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + +/** + * A {@code QuicPacketEncoder} encapsulates the logic to encode a + * quic packet. A {@code QuicPacketEncoder} is typically tied to + * a particular version of the QUIC protocol. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public class QuicPacketEncoder { + + private static final Logger debug = Utils.getDebugLogger(() -> "QuicPacketEncoder"); + + private final QuicVersion quicVersion; + private QuicPacketEncoder(final QuicVersion quicVersion) { + this.quicVersion = quicVersion; + } + + /** + * Computes the packet's header byte, which also encodes + * the packetNumber length. + * + * @param packetTypeTag quic-dependent packet type encoding + * @param pnsize the number of bytes needed to encode the packet number + * @return the packet's header byte + */ + private static byte headers(byte packetTypeTag, int pnsize) { + int pnprefix = pnsize - 1; + assert pnprefix >= 0; + assert pnprefix <= 3; + return (byte)(packetTypeTag | pnprefix); + } + + /** + * Returns the headers tag for the given packet type. + * Returns 0 if the packet type is NONE or unknown. + *

    + * For version negotiations packet, this method returns 0x80. + * The other 7 bits must be ignored by a client. + * When emitting a version negotiation packet the server should + * also set the fix bit (0x40) to 1. + * What distinguishes a version negotiation packet from other + * long header packet types is not the packet type found in the + * header's byte, but the fact that a. it is a long header and + * b. the version number in the packet (the 4 bytes following + * the header) is 0. + * @param packetType the packet type + * @return the headers tag for the given packet type. + * + * @see + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @see + * RFC 9369: QUIC Version 2 + */ + private byte packetHeadersTag(PacketType packetType) { + return (byte) switch (quicVersion) { + case QUIC_V1 -> switch (packetType) { + case ONERTT -> 0x40; + case INITIAL -> 0xC0; + case ZERORTT -> 0xD0; + case HANDSHAKE -> 0xE0; + case RETRY -> 0xF0; + case VERSIONS -> 0x80; // remaining bits are ignored + case NONE -> 0x00; + }; + case QUIC_V2 -> switch (packetType) { + case ONERTT -> 0x40; + case INITIAL -> 0xD0; + case ZERORTT -> 0xE0; + case HANDSHAKE -> 0xF0; + case RETRY -> 0xC0; + case VERSIONS -> 0x80; // remaining bits are ignored + case NONE -> 0x00; + }; + }; + } + + /** + * Encode the OneRttPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingOneRttPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + QuicConnectionId destination = packet.destinationId(); + + if (debug.on()) { + debug.log("OneRttPacket::encodePacket(ByteBuffer(%d,%d)," + + " dst=%s, packet=%d, encodedPacket=%s," + + " payload=QuicFrames(frames: %s, bytes: %d)," + + " size=%d", + buffer.position(), buffer.limit(), destination, + packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : "incorrect packet number prefix in headers: " + headers; + + final PacketWriter writer = new PacketWriter(buffer, context, PacketType.ONERTT); + writer.writeHeaders(headers); + writer.writeShortConnectionId(destination); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderShort(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the ZeroRttPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingZeroRttPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("ZeroRttPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, payload=QuicFrame(frames: %s, bytes: %d), size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.ZERORTT); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the VersionNegotiationPacket into the provided + * buffer. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @throws BufferOverflowException if the buffer is not large enough + */ + private static void encodePacket(OutgoingVersionNegotiationPacket packet, + ByteBuffer buffer) { + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, versions=%s, size=%d", + buffer.position(), buffer.limit(), source, destination, + Arrays.toString(packet.versions), packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int offset = buffer.position(); + int limit = buffer.limit(); + assert buffer.capacity() >= packet.size; + assert limit - offset >= packet.size; + + int typeTag = 0x80; + int rand = Encoders.RANDOM.nextInt() & 0x7F; + int headers = typeTag | rand; + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket:" + + " type: 0x%02x, unused: 0x%02x, headers: 0x%02x", + typeTag, rand & ~0x80, headers); + } + assert (headers & typeTag) == typeTag : headers; + assert (headers ^ typeTag) == rand : headers; + + // headers(1 byte), version(4 bytes) + buffer.put((byte)headers); // 1 + putInt32(buffer, 0); // 4 + + // DCID: 1 byte for length, + destination id bytes + var dcidlen = destination.length(); + assert dcidlen <= MAX_CONNECTION_ID_LENGTH && dcidlen >= 0 : dcidlen; + buffer.put((byte)dcidlen); // 1 + buffer.put(destination.asReadOnlyBuffer()); + assert buffer.position() == offset + 6 + dcidlen : buffer.position(); + + // SCID: 1 byte for length, + source id bytes + var scidlen = source.length(); + assert scidlen <= MAX_CONNECTION_ID_LENGTH && scidlen >= 0 : scidlen; + buffer.put((byte) scidlen); + buffer.put(source.asReadOnlyBuffer()); + assert buffer.position() == offset + 7 + dcidlen + scidlen : buffer.position(); + + // Put payload (= supported versions) + int versionsStart = buffer.position(); + for (int i = 0; i < packet.versions.length; i++) { + putInt32(buffer, packet.versions[i]); + } + int versionsEnd = buffer.position(); + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket:" + + " encoded %d bytes", offset - versionsEnd); + } + + assert versionsEnd - offset == packet.size; + assert versionsEnd - versionsStart == packet.versions.length << 2; + } + + /** + * Encode the HandshakePacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingHandshakePacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("HandshakePacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, payload=QuicFrame(frames: %s, bytes: %d)," + + " size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.HANDSHAKE); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the InitialPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context coding context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingInitialPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("InitialPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, token=%s, " + + "payload=QuicFrame(frames: %s, bytes: %d), size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.token == null ? null : "byte[%s]".formatted(packet.token.length), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.INITIAL); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writeToken(packet.token); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the RetryPacket into the provided buffer. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingRetryPacket packet, + ByteBuffer buffer, + CodingContext context) throws QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + + if (debug.on()) { + debug.log("RetryPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, retryToken=%d," + + " size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.retryToken.length, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + assert packet.retryToken.length > 0; + assert buffer.remaining() >= packet.size; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.RETRY); + + byte headers = packetHeadersTag(packet.packetType()); + headers |= (byte)Encoders.RANDOM.nextInt(0x10); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writeRetryToken(packet.retryToken); + assert writer.remaining() >= 16; // 128 bits + writer.signRetry(version); + + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + } + + public abstract static class OutgoingQuicPacket implements QuicPacket { + private final QuicConnectionId destinationId; + + protected OutgoingQuicPacket(QuicConnectionId destinationId) { + this.destinationId = destinationId; + } + + @Override + public final QuicConnectionId destinationId() { return destinationId; } + + @Override + public String toString() { + + return this.getClass().getSimpleName() + "[pn=" + this.packetNumber() + + ", frames=" + frames() + "]"; + } + } + + private abstract static class OutgoingShortHeaderPacket + extends OutgoingQuicPacket implements ShortHeaderPacket { + + OutgoingShortHeaderPacket(QuicConnectionId destinationId) { + super(destinationId); + } + } + + private abstract static class OutgoingLongHeaderPacket + extends OutgoingQuicPacket implements LongHeaderPacket { + + private final QuicConnectionId sourceId; + private final int version; + OutgoingLongHeaderPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version) { + super(destinationId); + this.sourceId = sourceId; + this.version = version; + } + + @Override + public final QuicConnectionId sourceId() { return sourceId; } + + @Override + public final int version() { return version; } + + } + + private static final class OutgoingRetryPacket + extends OutgoingLongHeaderPacket implements RetryPacket { + + final int size; + final byte[] retryToken; + + OutgoingRetryPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + byte[] retryToken) { + super(sourceId, destinationId, version); + this.retryToken = retryToken; + this.size = computeSize(retryToken.length); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the end of the retry integrity tag. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @return the total packet size. + */ + private int computeSize(int tokenLength) { + assert tokenLength > 0; + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), + // retryTokenIntegrity(128 bits) => 7 + 16 = 23 bytes + int size = Math.addExact(23, tokenLength); + size = Math.addExact(size, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + return size; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] retryToken() { + return retryToken; + } + } + + private static final class OutgoingHandshakePacket + extends OutgoingLongHeaderPacket implements HandshakePacket { + + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + final int payloadSize; + private int tagSize; + + OutgoingHandshakePacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.payloadSize = frames.stream().mapToInt(QuicFrame::size).reduce(0, Math::addExact); + this.tagSize = tagSize; + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computeSize(length); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param length The value of the length header + * + * @return the total packet size. + */ + private int computeSize(int length) { + assert length >= 0; + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + size = Math.addExact(size, lnsize); + size = Math.addExact(size, length); + return size; + } + + @Override + public List frames() { return frames; } + + } + + private static final class OutgoingZeroRttPacket + extends OutgoingLongHeaderPacket implements ZeroRttPacket { + + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + OutgoingZeroRttPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computeSize(length); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param length The value of the length header + * + * @return the total packet size. + */ + private int computeSize(int length) { + assert length >= 0; + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + size = Math.addExact(size, lnsize); + size = Math.addExact(size, length); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingOneRttPacket + extends OutgoingShortHeaderPacket implements OneRttPacket { + + final long packetNumber; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + OutgoingOneRttPacket(QuicConnectionId destinationId, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(destinationId); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.size = computeSize(payloadSize, encodedPacketNumber.length, tagSize); + } + + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param payloadSize The size of the packet's payload + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag + * @return the total packet size. + */ + private int computeSize(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + // Fixed size bits: + // headers(1 byte) + int size = Math.addExact(1, destinationId().length()); + + size = Math.addExact(size, payloadSize); + size = Math.addExact(size, pnsize); + size = Math.addExact(size, tagSize); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingInitialPacket + extends OutgoingLongHeaderPacket implements InitialPacket { + + final byte[] token; + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + private record InitialPacketVariableComponents(int length, byte[] token, QuicConnectionId sourceId, + QuicConnectionId destinationId) { + + } + + public OutgoingInitialPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + byte[] token, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.token = token; + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computePacketSize(new InitialPacketVariableComponents(length, token, sourceId, + destinationId)); + } + + @Override + public int tokenLength() { return token == null ? 0 : token.length; } + + @Override + public byte[] token() { return token; } + + @Override + public int length() { return length; } + + @Override + public long packetNumber() { return packetNumber; } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { return size; } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private static int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param variableComponents The variable components of the packet + * + * @return the total packet size. + */ + private static int computePacketSize(InitialPacketVariableComponents variableComponents) { + assert variableComponents.length >= 0; + + // how many bytes are needed to encode the length of the token + final byte[] token = variableComponents.token; + int tkLenSpecifierSize = token == null || token.length == 0 + ? 1 : VariableLengthEncoder.getEncodedSize(token.length); + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(variableComponents.length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID length specifier(1 byte), + // SCID length specifier(1 byte), => 7 bytes + int size = Math.addExact(7, variableComponents.sourceId.length()); + size = Math.addExact(size, variableComponents.destinationId.length()); + size = Math.addExact(size, tkLenSpecifierSize); + if (token != null) { + size = Math.addExact(size, token.length); + } + size = Math.addExact(size, lnsize); + size = Math.addExact(size, variableComponents.length); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingVersionNegotiationPacket + extends OutgoingLongHeaderPacket + implements VersionNegotiationPacket { + + final int[] versions; + final int size; + final int payloadSize; + + public OutgoingVersionNegotiationPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int[] versions) { + super(sourceId, destinationId, 0); + this.versions = versions.clone(); + this.payloadSize = versions.length << 2; + this.size = computeSize(payloadSize); + } + + @Override + public int[] supportedVersions() { + return versions.clone(); + } + + @Override + public int size() { return size; } + + @Override + public int payloadSize() { return payloadSize; } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param payloadSize The size of the packet's payload + * @return the total packet size. + */ + private int computeSize(int payloadSize) { + assert payloadSize > 0; + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, payloadSize); + size = Math.addExact(size, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + return size; + } + + } + + /** + * Create a new unencrypted InitialPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param token The token field (may be null if no token) + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The initial packet payload + * + * @param codingContext + * @return the new initial packet + */ + public OutgoingQuicPacket newInitialPacket(QuicConnectionId source, + QuicConnectionId destination, + byte[] token, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newInitialPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 + // A client MUST expand the payload of all UDP datagrams carrying Initial packets + // to at least the smallest allowed maximum datagram size of 1200 bytes + // by adding PADDING frames to the Initial packet or by coalescing the Initial packet + + // first compute the packet size + final int originalPayloadSize = frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + final int originalLength = OutgoingInitialPacket.computeLength(originalPayloadSize, + encodedPacketNumber.length, tagSize); + final int originalPacketSize = OutgoingInitialPacket.computePacketSize( + new OutgoingInitialPacket.InitialPacketVariableComponents(originalLength, token, + source, destination)); + if (originalPacketSize >= 1200) { + return new OutgoingInitialPacket(source, destination, this.quicVersion.versionNumber(), + token, packetNumber, encodedPacketNumber, frames, tagSize); + } else { + // add padding + int numPaddingBytesNeeded = 1200 - originalPacketSize; + if (originalLength < 64 && originalLength + numPaddingBytesNeeded > 64) { + // if originalLength + numPaddingBytesNeeded == 64, will send + // 1201 bytes + numPaddingBytesNeeded--; + } + final List newFrames = new ArrayList<>(); + for (QuicFrame frame : frames) { + if (frame instanceof PaddingFrame) { + // a padding frame already exists, instead of including this and the new padding + // frame in the new frames, we just include 1 single padding frame whose + // combined size will be the sum of all existing padding frames and the + // additional padding bytes needed + numPaddingBytesNeeded += frame.size(); + continue; + } + // non-padding frame, include it in the new frames + newFrames.add(frame); + } + // add the padding frame as the first frame + newFrames.add(0, new PaddingFrame(numPaddingBytesNeeded)); + return new OutgoingInitialPacket( + source, destination, this.quicVersion.versionNumber(), + token, packetNumber, encodedPacketNumber, newFrames, tagSize); + } + } + + /** + * Create a new unencrypted VersionNegotiationPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param versions The supported quic versions + * @return the new initial packet + */ + public static OutgoingQuicPacket newVersionNegotiationPacket(QuicConnectionId source, + QuicConnectionId destination, + int[] versions) { + return new OutgoingVersionNegotiationPacket(source, destination, versions); + } + + /** + * Create a new unencrypted RetryPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param retryToken The retry token + * @return the new retry packet + */ + public OutgoingQuicPacket newRetryPacket(QuicConnectionId source, + QuicConnectionId destination, + byte[] retryToken) { + return new OutgoingRetryPacket( + source, destination, this.quicVersion.versionNumber(), retryToken); + } + + /** + * Create a new unencrypted ZeroRttPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The zero RTT packet payload + * @param codingContext + * @return the new zero RTT packet + */ + public OutgoingQuicPacket newZeroRttPacket(QuicConnectionId source, + QuicConnectionId destination, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newZeroRttPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.ZERO_RTT); + int minLength = 4 + protectionSampleSize - encodedPacketNumber.length - tagSize; + + return new OutgoingZeroRttPacket( + source, destination, this.quicVersion.versionNumber(), packetNumber, + encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Create a new unencrypted HandshakePacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param frames The handshake packet payload + * @param codingContext + * @return the new handshake packet + */ + public OutgoingQuicPacket newHandshakePacket(QuicConnectionId source, + QuicConnectionId destination, + long packetNumber, + long largestAckedPN, + List frames, CodingContext codingContext) { + if (debug.on()) { + debug.log("newHandshakePacket: fullPN=%d ackedPN=%d", + packetNumber, largestAckedPN); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, largestAckedPN); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.HANDSHAKE); + int minLength = 4 + protectionSampleSize - encodedPacketNumber.length - tagSize; + + return new OutgoingHandshakePacket( + source, destination, this.quicVersion.versionNumber(), + packetNumber, encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Create a new unencrypted OneRttPacket to be transmitted over the wire + * after encryption. + * + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The one RTT packet payload + * @param codingContext + * @return the new one RTT packet + */ + public OneRttPacket newOneRttPacket(QuicConnectionId destination, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newOneRttPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.ONE_RTT); + // packets should be at least 22 bytes longer than the local connection id length. + // we ensure that by padding the frames to the necessary size + int minPayloadSize = codingContext.minShortPacketPayloadSize(destination.length()); + assert protectionSampleSize == tagSize; + int minLength = Math.max(5, minPayloadSize) - encodedPacketNumber.length; + return new OutgoingOneRttPacket( + destination, packetNumber, + encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Creates a packet in the given keyspace for the purpose of sending + * a CONNECTION_CLOSE, or a generic list of frames. + * The {@code initialToken} parameter is ignored if the key + * space is not INITIAL. + * + * @param keySpace the sending key space + * @param packetSpace the packet space + * @param sourceId the source connection id + * @param destinationId the destination connection id + * @param initialToken the initial token for INITIAL packets + * @param frames the list of frames + * @param codingContext the coding context + * @return a packet in the given key space + * @throws IllegalArgumentException if the packet number space is + * not one of INITIAL, HANDSHAKE, or APPLICATION + */ + public OutgoingQuicPacket newOutgoingPacket( + KeySpace keySpace, + PacketSpace packetSpace, + QuicConnectionId sourceId, + QuicConnectionId destinationId, + byte[] initialToken, + List frames, CodingContext codingContext) { + long largestAckedPN = packetSpace.getLargestPeerAckedPN(); + return switch (packetSpace.packetNumberSpace()) { + case APPLICATION -> { + long newPacketNumber = packetSpace.allocateNextPN(); + if (keySpace == KeySpace.ZERO_RTT) { + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.ZERORTT)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + yield newZeroRttPacket(sourceId, + destinationId, + newPacketNumber, + largestAckedPN, + frames, + codingContext); + } else { + assert keySpace == KeySpace.ONE_RTT; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.ONERTT)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + final OneRttPacket oneRttPacket = newOneRttPacket(destinationId, + newPacketNumber, + largestAckedPN, + frames, + codingContext); + assert oneRttPacket instanceof OutgoingOneRttPacket : + "unexpected 1-RTT packet type: " + oneRttPacket.getClass(); + yield (OutgoingQuicPacket) oneRttPacket; + } + } + case HANDSHAKE -> { + assert keySpace == KeySpace.HANDSHAKE; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.HANDSHAKE)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + long newPacketNumber = packetSpace.allocateNextPN(); + yield newHandshakePacket(sourceId, destinationId, + newPacketNumber, largestAckedPN, + frames, codingContext); + } + case INITIAL -> { + assert keySpace == KeySpace.INITIAL; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.INITIAL)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + long newPacketNumber = packetSpace.allocateNextPN(); + yield newInitialPacket(sourceId, destinationId, + initialToken, newPacketNumber, + largestAckedPN, + frames, codingContext); + } + case NONE -> { + throw new IllegalArgumentException("packetSpace: %s, keySpace: %s" + .formatted(packetSpace.packetNumberSpace(), keySpace)); + } + }; + } + + /** + * Encodes the given QuicPacket. + * + * @param packet the packet to encode + * @param buffer the byte buffer to write the packet into + * @param context context for encoding + * @throws IllegalArgumentException if the packet is not an OutgoingQuicPacket, + * or if the packet version does not match the encoder version + * @throws BufferOverflowException if the buffer is not large enough + * @throws QuicKeyUnavailableException if the packet could not be encrypted + * because the required encryption key is not available + * @throws QuicTransportException if encrypting the packet resulted + * in an error that requires closing the connection + */ + public void encode(QuicPacket packet, ByteBuffer buffer, CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + switch (packet) { + case OutgoingOneRttPacket p -> encodePacket(p, buffer, context); + case OutgoingZeroRttPacket p -> encodePacket(p, buffer, context); + case OutgoingVersionNegotiationPacket p -> encodePacket(p, buffer); + case OutgoingHandshakePacket p -> encodePacket(p, buffer, context); + case OutgoingInitialPacket p -> encodePacket(p, buffer, context); + case OutgoingRetryPacket p -> encodePacket(p, buffer, context); + default -> throw new IllegalArgumentException("packet is not an outgoing packet: " + + packet.getClass()); + } + } + + /** + * Compute the max size of the usable payload of an initial + * packet, given the max size of the datagram. + *

    +     * Initial Packet {
    +     *     Header (1 byte),
    +     *     Version (4 bytes),
    +     *     Destination Connection ID Length (1 byte),
    +     *     Destination Connection ID (0..20 bytes),
    +     *     Source Connection ID Length (1 byte),
    +     *     Source Connection ID (0..20 bytes),
    +     *     Token Length (variable int),
    +     *     Token (..),
    +     *     Length (variable int),
    +     *     Packet Number (1..4 bytes),
    +     *     Packet Payload (1 to ... bytes),
    +     * }
    +     * 
    + * + * @param codingContext the coding context, used to compute the + * encoded packet number + * @param pnsize packet number length + * @param tokenLength the length of the token (or {@code 0}) + * @param scidLength the length of the source connection id + * @param dstidLength the length of the destination connection id + * @param maxDatagramSize the desired total maximum size + * of the packet after encryption + * @return the maximum size of the payload that can be fit into this + * initial packet + */ + public static int computeMaxInitialPayloadSize(CodingContext codingContext, + int pnsize, + int tokenLength, + int scidLength, + int dstidLength, + int maxDatagramSize) { + // header=1, version=4, len(scidlen)+len(dstidlen)=2 + int overhead = 1 + 4 + 2 + scidLength + dstidLength + tokenLength + + VariableLengthEncoder.getEncodedSize(tokenLength); + // encryption tag, included in the payload, but not usable for frames + int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + int length = maxDatagramSize - overhead - 1; // at least 1 byte for length encoding + if (length <= 0) return 0; + int lenbefore = VariableLengthEncoder.getEncodedSize(length); + length = length - lenbefore + 1; // discount length encoding + // int lenafter = VariableLengthEncoder.getEncodedSize(length); // check + // assert lenafter == lenbefore : "%s -> %s (before:%s, after:%s)" + // .formatted(maxDatagramSize - overhead -1, length, lenbefore, lenafter); + if (length <= 0) return 0; + int available = length - pnsize - tagSize; + if (available < 0) return 0; + return available; + } + + /** + * Compute the max size of the usable payload of a handshake + * packet, given the max size of the datagram. + *
    +     * Initial Packet {
    +     *     Header (1 byte),
    +     *     Version (4 bytes),
    +     *     Destination Connection ID Length (1 byte),
    +     *     Destination Connection ID (0..20 bytes),
    +     *     Source Connection ID Length (1 byte),
    +     *     Source Connection ID (0..20 bytes),
    +     *     Length (variable int),
    +     *     Packet Number (1..4 bytes),
    +     *     Packet Payload (1 to ... bytes),
    +     * }
    +     * 
    + * @param codingContext the coding context, used to compute the + * encoded packet number + * @param packetNumber the full packet number + * @param scidLength the length of the source connection id + * @param dstidLength the length of the destination connection id + * @param maxDatagramSize the desired total maximum size + * of the packet after encryption + * @return the maximum size of the payload that can be fit into this + * initial packet + */ + public static int computeMaxHandshakePayloadSize(CodingContext codingContext, + long packetNumber, + int scidLength, + int dstidLength, + int maxDatagramSize) { + // header=1, version=4, len(scidlen)+len(dstidlen)=2 + int overhead = 1 + 4 + 2 + scidLength + dstidLength; + int pnsize = computePacketNumberLength(packetNumber, + codingContext.largestAckedPN(PacketNumberSpace.HANDSHAKE)); + // encryption tag, included in the payload, but not usable for frames + int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + int length = maxDatagramSize - overhead -1; // at least 1 byte for length encoding + if (length < 0) return 0; + int lenbefore = VariableLengthEncoder.getEncodedSize(length); + length = length - lenbefore + 1; // discount length encoding + int available = length - pnsize - tagSize; + return available; + } + + /** + * Computes the maximum usable payload that can be carried on in a + * {@link OneRttPacket} given the max datagram size before + * encryption. + * @param codingContext the coding context + * @param packetNumber the packet number + * @param dstidLength the peer connection id length + * @param maxDatagramSizeBeforeEncryption the maximum size of the datagram + * @return the maximum payload that can be carried on in a + * {@link OneRttPacket} given the max datagram size before + * encryption + */ + public static int computeMaxOneRTTPayloadSize(final CodingContext codingContext, + final long packetNumber, + final int dstidLength, + final int maxDatagramSizeBeforeEncryption, + final long largestPeerAckedPN) { + // header=1 + final int overhead = 1 + dstidLength; + // always reserve four bytes for packet number to avoid issues with packet + // sizes when retransmitting. This is a hack, but it avoids having to + // repack StreamFrames. + final int pnsize = 4; //computePacketNumberLength(packetNumber, largestPeerAckedPN); + // encryption tag, included in the payload, but not usable for frames + final int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + final int available = maxDatagramSizeBeforeEncryption - overhead - pnsize - tagSize; + if (available < 0) return 0; + return available; + } + + private static ByteBuffer putInt32(ByteBuffer buffer, int value) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.putInt(value); + } + + + /** + * A {@code PacketWriter} to write a Quic packet. + *

    + * A {@code PacketWriter} offers high level helper methods to write + * data (such as Connection IDs or Packet Numbers) from a Quic packet. + * It has however no or little knowledge of the actual packet structure. + * It is driven by the {@code encode} method of the appropriate + * {@code OutgoingQuicPacket} type. + *

    + * A {@code PacketWriter} is stateful: it encapsulates a {@code ByteBuffer} + * (or possibly a list of byte buffers - as a future enhancement) and + * advances the position on the buffer it is writing. + * + */ + static class PacketWriter { + final ByteBuffer buffer; + final int offset; + final int initialLimit; + final CodingContext context; + final PacketType packetType; + + PacketWriter(ByteBuffer buffer, CodingContext context, PacketType packetType) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int pos = buffer.position(); + int limit = buffer.limit(); + this.buffer = buffer; + this.offset = pos; + this.initialLimit = limit; + this.context = context; + this.packetType = packetType; + } + + public int offset() { + return offset; + } + + public int position() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public int bytesWritten() { + return position() - offset; + } + + public void reset() { + buffer.position(offset); + buffer.limit(initialLimit); + } + + public byte headers() { + return buffer.get(offset); + } + + public void headers(byte headers) { + buffer.put(offset, headers); + } + + public PacketType packetType() { + return packetType; + } + + public void writeHeaders(byte headers) { + buffer.put(headers); + } + + public void writeVersion(int version) { + buffer.putInt(version); + } + + public void writeSupportedVersions(int[] versions) { + for (int i=0 ; i= 0 && packetLength <= VariableLengthEncoder.MAX_ENCODED_INTEGER + : packetLength; + writeVariableLength(packetLength); + } + + private void writeTokenLength(long tokenLength) { + writeVariableLength(tokenLength); + } + + public void writeToken(byte[] token) { + if (token == null) { + buffer.put((byte)0); + } else { + writeTokenLength(token.length); + buffer.put(token); + } + } + + public void writeVariableLength(long value) { + VariableLengthEncoder.encode(buffer, value); + } + + private void maskPacketNumber(int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + public void writeEncodedPacketNumber(byte[] packetNumber) { + buffer.put(packetNumber); + } + + public void encryptPayload(final long packetNumber, final int payloadstart) + throws QuicTransportException, QuicKeyUnavailableException { + final int payloadend = buffer.position(); + buffer.position(payloadstart); // position the output buffer + final int payloadLength = payloadend - payloadstart; + final int headersLength = payloadstart - offset; + final ByteBuffer packetHeader = buffer.slice(offset, headersLength); + final ByteBuffer packetPayload = buffer.slice(payloadstart, payloadLength) + .asReadOnlyBuffer(); + try { + context.getTLSEngine().encryptPacket(packetType.keySpace().get(), packetNumber, + new HeaderGenerator(this.packetType, packetHeader), packetPayload, buffer); + } catch (ShortBufferException e) { + throw new QuicTransportException(e.toString(), null, 0, + QuicTransportErrors.INTERNAL_ERROR); + } + } + + public void writePayload(List frames) { + for (var frame : frames) frame.encode(buffer); + } + + public void writeLongConnectionId(QuicConnectionId connId) { + ByteBuffer src = connId.asReadOnlyBuffer(); + assert src.remaining() <= MAX_CONNECTION_ID_LENGTH; + buffer.put((byte)src.remaining()); + buffer.put(src); + } + + public void writeShortConnectionId(QuicConnectionId connId) { + ByteBuffer src = connId.asReadOnlyBuffer(); + assert src.remaining() <= MAX_CONNECTION_ID_LENGTH; + buffer.put(src); + } + + public void writeRetryToken(byte[] retryToken) { + buffer.put(retryToken); + } + + @Override + public String toString() { + return "PacketWriter(offset=%s, pos=%s, remaining=%s)" + .formatted(offset, position(), remaining()); + } + + public void protectHeaderLong(int packetNumberStart, int packetNumberLength) + throws QuicKeyUnavailableException, QuicTransportException { + protectHeader(packetNumberStart, packetNumberLength, (byte) 0x0f); + } + + public void protectHeaderShort(int packetNumberStart, int packetNumberLength) + throws QuicKeyUnavailableException, QuicTransportException { + protectHeader(packetNumberStart, packetNumberLength, (byte) 0x1f); + } + + private void protectHeader(int packetNumberStart, int packetNumberLength, byte headerMask) + throws QuicKeyUnavailableException, QuicTransportException { + // expect position at the end of packet + QuicTLSEngine tlsEngine = context.getTLSEngine(); + int sampleSize = tlsEngine.getHeaderProtectionSampleSize(packetType.keySpace().get()); + assert buffer.position() - packetNumberStart >= sampleSize + 4 : buffer.position() - packetNumberStart - sampleSize - 4; + + ByteBuffer sample = buffer.slice(packetNumberStart + 4, sampleSize); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(packetType.keySpace().get(), false, sample); + byte headers = headers(); + headers ^= (byte) (encryptedSample.get() & headerMask); + headers(headers); + maskPacketNumber(packetNumberStart, packetNumberLength, encryptedSample); + } + + private void signRetry(final int version) throws QuicTransportException { + final QuicVersion retryVersion = QuicVersion.of(version) + .orElseThrow(() -> new IllegalArgumentException("Unknown Quic version 0x" + + Integer.toHexString(version))); + int payloadend = buffer.position(); + ByteBuffer temp = buffer.asReadOnlyBuffer(); + temp.position(offset); + temp.limit(payloadend); + try { + context.getTLSEngine().signRetryPacket(retryVersion, + context.originalServerConnId().asReadOnlyBuffer(), temp, buffer); + } catch (ShortBufferException e) { + throw new QuicTransportException("Failed to sign packet", + null, 0, QuicTransportErrors.INTERNAL_ERROR); + } + } + + // generates packet header and is capable of inserting a key phase into the header + // when appropriate + private static final class HeaderGenerator implements IntFunction { + private final PacketType packetType; + private final ByteBuffer header; + + private HeaderGenerator(final PacketType packetType, final ByteBuffer header) { + this.packetType = packetType; + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyPhase) { + // we use key phase only in 1-RTT packet header + if (packetType != PacketType.ONERTT) { + assert keyPhase == 0 : "unexpected key phase " + keyPhase + + " for packet type " + packetType; + // return the packet header without setting any key phase bit + return header; + } + // update the key phase bit in the packet header + setKeyPhase(keyPhase); + return header.position(0).asReadOnlyBuffer(); + } + + private void setKeyPhase(final int kp) { + if (kp != 0 && kp != 1) { + throw new IllegalArgumentException("Invalid key phase: " + kp); + } + final byte headerFirstByte = this.header.get(); + final byte updated = (byte) (headerFirstByte | (kp << 2)); + this.header.put(0, updated); + } + } + } + + + /** + * Adds required padding frames if necessary. + * Needed to make sure there's enough bytes to apply header protection + * @param frames requested list of frames + * @param minLength requested minimum length + * @return list of frames that meets the minimum length requirement + */ + private static List padFrames(List frames, int minLength) { + if (frames.size() >= minLength) { + return frames; + } + int size = frames.stream().mapToInt(QuicFrame::size).reduce(0, Math::addExact); + if (size >= minLength) { + return frames; + } + List result = new ArrayList<>(frames.size() + 1); + // add padding frame in front - some frames extend to end of packet + result.add(new PaddingFrame(minLength - size)); + result.addAll(frames); + return result; + } + + /** + * Returns an encoder for the given Quic version. + * Returns {@code null} if no encoder for that version exists. + * + * @param quicVersion the Quic protocol version number + * @return an encoder for the given Quic version or {@code null} + */ + public static QuicPacketEncoder of(final QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> Encoders.QUIC_V1_ENCODER; + case QUIC_V2 -> Encoders.QUIC_V2_ENCODER; + default -> throw new IllegalArgumentException("No packet encoder for Quic version " + quicVersion); + }; + } + + private static final class Encoders { + static final Random RANDOM = new Random(); + static final QuicPacketEncoder QUIC_V1_ENCODER = new QuicPacketEncoder(QuicVersion.QUIC_V1); + static final QuicPacketEncoder QUIC_V2_ENCODER = new QuicPacketEncoder(QuicVersion.QUIC_V2); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java new file mode 100644 index 00000000000..197d46fc0b0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.nio.ByteBuffer; + +/** + * QUIC packet number encoding/decoding routines. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public class QuicPacketNumbers { + + /** + * Returns the number of bytes needed to encode a packet number + * given the full packet number and the largest ACK'd packet. + * + * @param fullPN the full packet number + * @param largestAcked the largest ACK'd packet, or -1 if none so far + * + * @throws IllegalArgumentException if number can't represented in 4 bytes + * @return the number of bytes required to encode the packet + */ + public static int computePacketNumberLength(long fullPN, long largestAcked) { + + long numUnAcked; + + if (largestAcked == -1) { + numUnAcked = fullPN + 1; + } else { + numUnAcked = fullPN - largestAcked; + } + + /* + * log(n, 2) + 1; ceil(minBits / 8); + * + * value will never be non-positive, so don't need to worry about the + * special cases. + */ + assert numUnAcked > 0 : "numUnAcked %s < 0 (fullPN: %s, largestAcked: %s)" + .formatted(numUnAcked, fullPN, largestAcked); + int minBits = 64 - Long.numberOfLeadingZeros(numUnAcked) + 1; + int numBytes = (minBits + 7) / 8; + + if (numBytes > 4) { + throw new IllegalArgumentException( + "Encoded packet number needs %s bytes for pn=%s, ack=%s" + .formatted(numBytes, fullPN, largestAcked)); + } + + return numBytes; + } + + /** + * Encode the full packet number against the largest ACK'd packet. + * + * Follows the algorithm outlined in + * + * RFC 9000. Appendix A.2 + * + * @param fullPN the full packet number + * @param largestAcked the largest ACK'd packet, or -1 if none so far + * + * @throws IllegalArgumentException if number can't be represented in 4 bytes + * @return byte array containing fullPN + */ + public static byte[] encodePacketNumber( + long fullPN, long largestAcked) { + + // throws IAE if more than 4 bytes are needed + int numBytes = computePacketNumberLength(fullPN, largestAcked); + assert numBytes <= 4 : numBytes; + return truncatePacketNumber(fullPN, numBytes); + } + + /** + * Truncate the full packet number to fill into {@code numBytes}. + * + * Follows the algorithm outlined in + * + * RFC 9000, Appendix A.2 + * + * @apiNote + * {@code numBytes} should have been computed using + * {@link #computePacketNumberLength(long, long)} + * + * @param fullPN the full packet number + * @param numBytes the number of bytes in which to encode + * the packet number + * + * @throws IllegalArgumentException if numBytes is out of range + * @return byte array containing fullPN + */ + public static byte[] truncatePacketNumber( + long fullPN, int numBytes) { + + if (numBytes <= 0 || numBytes > 4) { + throw new IllegalArgumentException( + "Invalid packet number length: " + numBytes); + } + + // Fill in the array. + byte[] retval = new byte[numBytes]; + for (int i = numBytes - 1; i >= 0; i--) { + retval[i] = (byte) (fullPN & 0xff); + fullPN = fullPN >>> 8; + } + + return retval; + } + + /** + * Decode the packet numbers against the largest ACK'd packet after header + * protection has been removed. + * + * Follows the algorithm outlined in + * + * RFC 9000, Appendix A.3 + * + * @param largestPN the largest packet number that has been successfully + * processed in the current packet number space + * @param buf a {@code ByteBuffer} containing the value of the + * Packet Number field + * @param pnNBytes the number of bytes indicated by the Packet + * Number Length field + * + * @throws java.nio.BufferUnderflowException if there is not enough data in the + * buffer + * @return the decoded packet number + */ + public static long decodePacketNumber( + long largestPN, ByteBuffer buf, int pnNBytes) { + + assert pnNBytes >= 1 && pnNBytes <= 4 + : "decodePacketNumber: " + pnNBytes; + + long truncatedPN = 0; + for (int i = 0; i < pnNBytes; i++) { + truncatedPN = (truncatedPN << 8) | (buf.get() & 0xffL); + } + + int pnNBits = pnNBytes * 8; + + long expectedPN = largestPN + 1L; + assert expectedPN >= 0 : "expectedPN: " + expectedPN; + long pnWin = 1L << pnNBits; + long pnHWin = pnWin / 2L; + long pnMask = pnWin - 1L; + + // The incoming packet number should be greater than + // expectedPN - pn_HWin and less than or equal to + // expectedPN + pn_HWin + // + // This means we cannot just strip the trailing bits from + // expectedPN and add the truncatedPN because that might + // yield a value outside the window. + // + // The following code calculates a candidate value and + // makes sure it's within the packet number window. + // Note the extra checks to prevent overflow and underflow. + long candidatePN = (expectedPN & ~pnMask) | truncatedPN; + + if ((candidatePN <= (expectedPN - pnHWin)) + && (candidatePN < ((1L << 62) - pnWin))) { + return candidatePN + pnWin; + } + + if ((candidatePN - pnHWin > expectedPN) + && (candidatePN >= pnWin)) { + return candidatePN - pnWin; + } + return candidatePN; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java new file mode 100644 index 00000000000..51638caf98c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This class models Quic Retry Packets, as defined by + * RFC 9000, Section 17.2.5: + * + *

    {@code
    + *    A Retry packet uses a long packet header with a type value of 0x03.
    + *    It carries an address validation token created by the server.
    + *    It is used by a server that wishes to perform a retry; see Section 8.1.
    + *
    + *    Retry Packet {
    + *      Header Form (1) = 1,
    + *      Fixed Bit (1) = 1,
    + *      Long Packet Type (2) = 3,
    + *      Unused (4),
    + *      Version (32),
    + *      Destination Connection ID Length (8),
    + *      Destination Connection ID (0..160),
    + *      Source Connection ID Length (8),
    + *      Source Connection ID (0..160),
    + *      Retry Token (..),
    + *      Retry Integrity Tag (128),
    + *    }
    + * }
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Retry Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see RFC 9000, Section 8.1/a> + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface RetryPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.RETRY; + } + + /** + * This packet type is not numbered: returns + * {@link PacketNumberSpace#NONE} always. + * @return {@link PacketNumberSpace#NONE} + */ + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.NONE; + } + + /** + * This packet type is not numbered: always returns -1L. + * @return -1L + */ + @Override + default long packetNumber() { return -1L; } + + /** + * {@return the packet's retry token} + * + * As per RFC 9000, Section 17.2.5: + *

    {@code
    +     *    An opaque token that the server can use to validate the client's address.
    +     * }
    + * + * @see + * RFC 9000, Section 8.1 + */ + byte[] retryToken(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java new file mode 100644 index 00000000000..a56689e6a0b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This interface models Quic Short Header packets, as defined by + * RFC 8999, Section 5.2: + * + *
    {@code
    + *    Short Header Packet {
    + *      Header Form (1) = 0,
    + *      Version-Specific Bits (7),
    + *      Destination Connection ID (..),
    + *      Version-Specific Data (..),
    + *    }
    + * }
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface ShortHeaderPacket extends QuicPacket { + @Override + default HeadersType headersType() { return HeadersType.SHORT; } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java new file mode 100644 index 00000000000..0ba5ba08c7e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This class models Quic Version Negotiation Packets, as defined by + * RFC 9000, Section 17.2.1: + * + *

    {@code
    + *    A Version Negotiation packet is inherently not version-specific.
    + *    Upon receipt by a client, it will be identified as a Version
    + *    Negotiation packet based on the Version field having a value of 0.
    + *
    + *    The Version Negotiation packet is a response to a client packet that
    + *    contains a version that is not supported by the server, and is only
    + *    sent by servers.
    + *
    + *    The layout of a Version Negotiation packet is:
    + *
    + *    Version Negotiation Packet {
    + *      Header Form (1) = 1,
    + *      Unused (7),
    + *      Version (32) = 0,
    + *      Destination Connection ID Length (8),
    + *      Destination Connection ID (0..2040),
    + *      Source Connection ID Length (8),
    + *      Source Connection ID (0..2040),
    + *      Supported Version (32) ...,
    + *    }
    + * }
    + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public interface VersionNegotiationPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { return PacketType.VERSIONS; } + @Override + default int version() { return 0;} + /** + * This packet type is not numbered: returns + * {@link PacketNumberSpace#NONE} always. + * @return {@link PacketNumberSpace#NONE} + */ + @Override + default PacketNumberSpace numberSpace() { return PacketNumberSpace.NONE; } + /** + * This packet type is not numbered: returns -1L always. + * @return -1L + */ + @Override + default long packetNumber() { return -1L; } + int[] supportedVersions(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java new file mode 100644 index 00000000000..c680aae1486 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic 0-RTT Packets, as defined by + * RFC 9000, Section 17.2.3: + * + *
    {@code
    + *    A 0-RTT packet uses long headers with a type value of 0x01, followed
    + *    by the Length and Packet Number fields; see Section 17.2. The first
    + *    byte contains the Reserved and Packet Number Length bits; see Section 17.2.
    + *    A 0-RTT packet is used to carry "early" data from the client to the server
    + *    as part of the first flight, prior to handshake completion. As part of the
    + *    TLS handshake, the server can accept or reject this early data.
    + *
    + *    See Section 2.3 of [TLS13] for a discussion of 0-RTT data and its limitations.
    +
    + *    0-RTT Packet {
    + *      Header Form (1) = 1,
    + *      Fixed Bit (1) = 1,
    + *      Long Packet Type (2) = 1,
    + *      Reserved Bits (2),
    + *      Packet Number Length (2),
    + *      Version (32),
    + *      Destination Connection ID Length (8),
    + *      Destination Connection ID (0..160),
    + *      Source Connection ID Length (8),
    + *      Source Connection ID (0..160),
    + *      Length (i),
    + *      Packet Number (8..32),
    + *      Packet Payload (..),
    + *    }
    + * } 
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same 0-RTT Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see + * RFC 9000, Section 17.2 + * + * @see + * [TLS13] RFC 8446, Section 2.3 + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface ZeroRttPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.ZERORTT; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.APPLICATION; + } + + @Override + default boolean hasLength() { return true; } + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java new file mode 100644 index 00000000000..e1a7fce1059 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * An abstract class to model a QuicStream. + * A quic stream can be either unidirectional + * or bidirectional. A unidirectional stream can + * be opened for reading or for writing. + * Concrete subclasses of {@code AbstractQuicStream} should + * implement {@link QuicSenderStream} (unidirectional {@link + * StreamMode#WRITE_ONLY} stream), or {@link QuicReceiverStream} + * (unidirectional {@link StreamMode#READ_ONLY} stream), or + * both (bidirectional {@link StreamMode#READ_WRITE} stream). + */ +abstract sealed class AbstractQuicStream implements QuicStream + permits QuicBidiStreamImpl, QuicSenderStreamImpl, QuicReceiverStreamImpl { + + private final QuicConnectionImpl connection; + private final long streamId; + private final StreamMode mode; + + AbstractQuicStream(QuicConnectionImpl connection, long streamId) { + this.mode = mode(connection, streamId); + this.streamId = streamId; + this.connection = connection; + } + + private static StreamMode mode(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return StreamMode.READ_WRITE; + if (connection.isClientConnection()) { + return QuicStreams.isClientInitiated(streamId) + ? StreamMode.WRITE_ONLY : StreamMode.READ_ONLY; + } else { + return QuicStreams.isClientInitiated(streamId) + ? StreamMode.READ_ONLY : StreamMode.WRITE_ONLY; + } + } + + /** + * {@return the {@code QuicConnectionImpl} instance this stream + * belongs to} + */ + final QuicConnectionImpl connection() { + return connection; + } + + @Override + public final long streamId() { + return streamId; + } + + @Override + public final StreamMode mode() { + return mode; + } + + @Override + public final boolean isClientInitiated() { + return QuicStreams.isClientInitiated(type()); + } + + @Override + public final boolean isServerInitiated() { + return QuicStreams.isServerInitiated(type()); + } + + @Override + public final boolean isBidirectional() { + return QuicStreams.isBidirectional(type()); + } + + @Override + public final boolean isLocalInitiated() { + return connection().isClientConnection() == isClientInitiated(); + } + + @Override + public final boolean isRemoteInitiated() { + return connection().isClientConnection() != isClientInitiated(); + } + + @Override + public final int type() { + return QuicStreams.streamType(streamId); + } + + /** + * {@return true if this stream isn't expecting anything + * from the peer and can be removed from the streams map} + */ + public abstract boolean isDone(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java new file mode 100644 index 00000000000..cbbbf6d083d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Queue; + +/** + * Class that buffers crypto data received from QuicTLSEngine. + * Generates CryptoFrames of requested size. + * + * Normally the frames are produced sequentially. However, when the client + * receives a Retry packet or a Version Negotiation packet, the client hello + * needs to be replayed. In that case we need to keep the processed data + * in the queues. + */ +public class CryptoWriterQueue { + private final Queue queue = new ArrayDeque<>(); + private long position = 0; + // amount of bytes remaining across all the enqueued buffers + private int totalRemaining = 0; + private boolean keepReplayData; + + /** + * Notify the writer to start keeping processed data. Can only be called on a fresh writer. + * @throws IllegalStateException if some data was processed already + */ + public synchronized void keepReplayData() { + if (position > 0) { + throw new IllegalStateException("Some data was processed already"); + } + keepReplayData = true; + } + + /** + * Notify the writer to stop keeping processed data. + */ + public synchronized void discardReplayData() { + if (!keepReplayData) { + return; + } + keepReplayData = false; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + ByteBuffer next = iterator.next(); + if (next.remaining() == 0) { + iterator.remove(); + } else { + return; + } + } + } + + /** + * Rewinds the enqueued buffer positions to allow for replaying the data + * @throws IllegalStateException if replay data is not available + */ + public synchronized void replayData() { + if (!keepReplayData) { + throw new IllegalStateException("Replay data not available"); + } + if (position == 0) { + return; + } + int rewound = 0; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + ByteBuffer next = iterator.next(); + if (next.position() != 0) { + rewound += next.position(); + next.position(0); + } else { + break; + } + } + assert rewound == position : rewound - position; + position = 0; + totalRemaining += rewound; + } + + /** + * Clears the queue and resets position back to zero + */ + public synchronized void reset() { + position = 0; + totalRemaining = 0; + queue.clear(); + } + + /** + * Enqueues the provided crypto data + * @param buffer data to enqueue + */ + public synchronized void enqueue(ByteBuffer buffer) { + queue.add(buffer.slice()); + totalRemaining += buffer.remaining(); + } + + /** + * Stores the next portion of queued crypto data in a frame. + * May return null if there's no data to enqueue or if + * maxSize is too small to fit at least one byte of data. + * The produced frame may be shorter than maxSize even if there are + * remaining bytes. + * @param maxSize maximum size of the returned frame, in bytes + * @return frame with next portion of crypto data, or null + * @throws IllegalArgumentException if maxSize < 0 + */ + public synchronized CryptoFrame produceFrame(int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException("negative maxSize"); + } + if (totalRemaining == 0) { + return null; + } + int posLength = VariableLengthEncoder.getEncodedSize(position); + // 1 (type) + posLength (position) + 1 (length) + 1 (payload) + if (maxSize < 3 + posLength) { + return null; + } + int maxPayloadPlusLen = maxSize - 1 - posLength; + int maxPayload; + if (maxPayloadPlusLen <= 64) { //63 bytes + 1 byte for length + maxPayload = maxPayloadPlusLen - 1; + } else if (maxPayloadPlusLen <= 16385) { // 16383 bytes + 2 bytes for length + maxPayload = maxPayloadPlusLen - 2; + } else { // 4 bytes for length + maxPayload = maxPayloadPlusLen - 4; + } + // the frame length that we decide upon + final int computedFrameLength = Math.min(maxPayload, totalRemaining); + assert computedFrameLength > 0 : computedFrameLength; + ByteBuffer frameData = null; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + final ByteBuffer buffer = iterator.next(); + // amount of remaining bytes in the current bytebuffer being processed + final int numRemainingInBuffer = buffer.remaining(); + if (numRemainingInBuffer == 0) { + if (!keepReplayData) { + iterator.remove(); + } + continue; + } + if (frameData == null) { + frameData = ByteBuffer.allocate(computedFrameLength); + } + if (frameData.remaining() >= numRemainingInBuffer) { + // frame data can accommodate the entire buffered data, so copy it over + frameData.put(buffer); + if (!keepReplayData) { + iterator.remove(); + } + } else { + // target frameData buffer cannot accommodate the entire buffered data, + // so we copy over only that much that the target buffer can accommodate + + // amount of data available in the target buffer + final int spaceAvail = frameData.remaining(); + // copy over the buffered data into the target frameData buffer + frameData.put(frameData.position(), buffer, buffer.position(), spaceAvail); + // manually move the position of the target buffer to account for the copied data + frameData.position(frameData.position() + spaceAvail); + // manually move the position of the (input) buffered data to account for + // data that we just copied + buffer.position(buffer.position() + spaceAvail); + // target frameData buffer is fully populated, no more processing of available + // input buffer necessary in this round + break; + } + } + assert frameData != null; + assert !frameData.hasRemaining() : frameData.remaining(); + frameData.flip(); + long oldPosition = position; + position += computedFrameLength; + totalRemaining -= computedFrameLength; + assert totalRemaining >= 0 : totalRemaining; + assert totalRemaining > 0 || keepReplayData || queue.isEmpty(); + return new CryptoFrame(oldPosition, computedFrameLength, frameData); + } + + /** + * {@return the current number of buffered bytes} + */ + public synchronized int remaining() { + return totalRemaining; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java new file mode 100644 index 00000000000..2185507d0db --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +/** + * An interface that represents a bidirectional stream. + * A bidirectional stream implements both {@link QuicSenderStream} + * and {@link QuicReceiverStream}. + */ +public non-sealed interface QuicBidiStream extends QuicStream, QuicReceiverStream, QuicSenderStream { + + /** + * The state of a bidirectional stream can be obtained by combining + * the state of its sending part and receiving part. + * + * A bidirectional stream is composed of sending and receiving + * parts. Implementations can represent states of the bidirectional + * stream as composites of sending and receiving stream states. + * The simplest model presents the stream as "open" when either + * sending or receiving parts are in a non-terminal state and + * "closed" when both sending and receiving streams are in + * terminal states. + * + * See RFC 9000, [Section 3.4] + * (https://www.rfc-editor.org/rfc/rfc9000#name-bidirectional-stream-states) + */ + enum BidiStreamState implements QuicStream.StreamState { + /** + * A bidirectional stream is considered "idle" if no + * data has been sent or received on that stream. + */ + IDLE, + /** + * A bidirectional stream is considered "open" until all data + * has been received, or all data has been sent, and no reset + * has been sent or received. + */ + OPENED, + /** + * A bidirectional stream is considered locally half closed + * if the sending part is locally closed: + * all data has been sent and acknowledged, or a reset has + * been sent, but the receiving part is still receiving. + */ + HALF_CLOSED_LOCAL, + /** + * A bidirectional stream is considered remotely half closed + * if the receiving part is closed: + * all data has been read or received on the receiving part, + * or reset has been read or received on the receiving part, but + * the sending part is still sending. + */ + HALF_CLOSED_REMOTE, + /** + * A bidirectional stream is considered closed when both parts + * have been reset or all data has been sent and acknowledged + * and all data has been received. + */ + CLOSED; + + /** + * @inheritDoc + * @apiNote + * A bidirectional stream may be considered closed (which is a terminal state), + * even if the sending or receiving part of a stream haven't reached a terminal + * state. Typically, if the sending part has sent a RESET frame, the stream + * may be considered closed even if the acknowledgement hasn't been received + * yet. + */ + @Override + public boolean isTerminal() { + return this == CLOSED; + } + } + + /** + * {@return a composed simplified state computed from the state of + * the receiving part and sending part of the stream} + *

    + * See RFC 9000, [Section 3.4] + * (https://www.rfc-editor.org/rfc/rfc9000#name-bidirectional-stream-states) + */ + default BidiStreamState getBidiStreamState() { + return switch (sendingState()) { + case READY -> switch (receivingState()) { + case RECV -> dataReceived() == 0 + ? BidiStreamState.IDLE + : BidiStreamState.OPENED; + case SIZE_KNOWN -> BidiStreamState.OPENED; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.HALF_CLOSED_REMOTE; + }; + case SEND, DATA_SENT -> switch (receivingState()) { + case RECV, SIZE_KNOWN -> BidiStreamState.OPENED; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.HALF_CLOSED_REMOTE; + }; + case DATA_RECVD, RESET_RECVD, RESET_SENT -> switch (receivingState()) { + case RECV, SIZE_KNOWN -> BidiStreamState.HALF_CLOSED_LOCAL; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.CLOSED; + }; + }; + } + + @Override + default StreamState state() { return getBidiStreamState(); } + + @Override + default boolean hasError() { + return rcvErrorCode() >= 0 || sndErrorCode() >= 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java new file mode 100644 index 00000000000..a964d96ef9c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * An implementation of a bidirectional stream. + * A bidirectional stream implements both {@link QuicSenderStream} + * and {@link QuicReceiverStream}. + */ +public final class QuicBidiStreamImpl extends AbstractQuicStream implements QuicBidiStream { + + // The sender part of this bidirectional stream + private final QuicSenderStreamImpl senderPart; + + // The receiver part of this bidirectional stream + private final QuicReceiverStreamImpl receiverPart; + + QuicBidiStreamImpl(QuicConnectionImpl connection, long streamId) { + this(connection, streamId, new QuicSenderStreamImpl(connection, streamId), + new QuicReceiverStreamImpl(connection, streamId)); + } + + private QuicBidiStreamImpl(QuicConnectionImpl connection, long streamId, + QuicSenderStreamImpl sender, QuicReceiverStreamImpl receiver) { + super(connection, streamId); + this.senderPart = sender; + this.receiverPart = receiver; + assert isBidirectional(); + } + + @Override + public ReceivingStreamState receivingState() { + return receiverPart.receivingState(); + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + return receiverPart.connectReader(scheduler); + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + receiverPart.disconnectReader(reader); + } + + @Override + public void requestStopSending(long errorCode) { + receiverPart.requestStopSending(errorCode); + } + + @Override + public boolean isStopSendingRequested() { + return receiverPart.isStopSendingRequested(); + } + + @Override + public long dataReceived() { + return receiverPart.dataReceived(); + } + + @Override + public long maxStreamData() { + return receiverPart.maxStreamData(); + } + + @Override + public SendingStreamState sendingState() { + return senderPart.sendingState(); + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return senderPart.connectWriter(scheduler); + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + senderPart.disconnectWriter(writer); + } + + @Override + public void reset(long errorCode) throws IOException { + senderPart.reset(errorCode); + } + + @Override + public long dataSent() { + return senderPart.dataSent(); + } + + /** + * {@return the sender part implementation of this bidirectional stream} + */ + public QuicSenderStreamImpl senderPart() { + return senderPart; + } + + /** + * {@return the receiver part implementation of this bidirectional stream} + */ + public QuicReceiverStreamImpl receiverPart() { + return receiverPart; + } + + @Override + public boolean isDone() { + return receiverPart.isDone() && senderPart.isDone(); + } + + @Override + public long rcvErrorCode() { + return receiverPart.rcvErrorCode(); + } + + @Override + public long sndErrorCode() { + return senderPart.sndErrorCode(); + } + + @Override + public boolean stopSendingReceived() { + return senderPart.stopSendingReceived(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java new file mode 100644 index 00000000000..3b6198682df --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java @@ -0,0 +1,1590 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicStreamLimitException; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState; +import jdk.internal.net.http.quic.streams.QuicStream.StreamMode; +import jdk.internal.net.http.quic.streams.QuicStream.StreamState; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.quic.QuicTransportErrors; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static jdk.internal.net.http.quic.streams.QuicStreams.*; + +/** + * A helper class to help manage Quic streams in a Quic connection + */ +public final class QuicConnectionStreams { + + // the (sliding) window size of MAX_STREAMS limit + private static final long MAX_BIDI_STREAMS_WINDOW_SIZE = QuicConnectionImpl.DEFAULT_MAX_BIDI_STREAMS; + private static final long MAX_UNI_STREAMS_WINDOW_SIZE = QuicConnectionImpl.DEFAULT_MAX_UNI_STREAMS; + + // These atomic long ids record the expected next stream ID that + // should be allocated for the next stream of a given type. + // The type of a stream is a number in [0..3], and is used + // as an index in this list. + private final List nextStreamID = List.of( + new AtomicLong(), // 0: client initiated bidi + new AtomicLong(SRV_MASK), // 1: server initiated bidi + new AtomicLong(UNI_MASK), // 2: client initiated uni + new AtomicLong(UNI_MASK | SRV_MASK)); // 3: server initiated uni + + // the max uni streams that the current endpoint is allowed to initiate against the peer + private final StreamCreationPermit localUniMaxStreamLimit = new StreamCreationPermit(0); + // the max bidi streams that the current endpoint is allowed to initiate against the peer + private final StreamCreationPermit localBidiMaxStreamLimit = new StreamCreationPermit(0); + // the max uni streams that the remote peer is allowed to initiate against the current endpoint + private final AtomicLong remoteUniMaxStreamLimit = new AtomicLong(0); + // the max bidi streams that the remote peer is allowed to initiate against the current endpoint + private final AtomicLong remoteBidiMaxStreamLimit = new AtomicLong(0); + + private final StreamsContainer streams = new StreamsContainer(); + + // A collection of senders which have available data ready to send, (or which possibly + // are blocked and need to send STREAM_DATA_BLOCKED). + // A stream stays in the queue until it is blocked or until it + // has no more data available to send: when a stream has no more data available for sending it is not + // put back in the queue. It will be put in the queue again when selectForSending is called. + private final ReadyStreamCollection sendersReady; + + // A map that contains streams for which sending a RESET_STREAM frame was requested + // and their corresponding error codes. + // Once the frame has been sent (or has been scheduled to be sent) the stream removed from the map. + private final ConcurrentMap sendersReset = new ConcurrentHashMap<>(); + + // A map that contains streams for which sending a MAX_STREAM_DATA frame was requested. + // Once the frame has been sent (or has been scheduled to be sent) the stream removed from the map. + private final ConcurrentMap receiversSend = new ConcurrentHashMap<>(); + + // A queue of remote initiated streams that have not been acquired yet. + // see pollNewRemoteStreams and addRemoteStreamListener + private final ConcurrentLinkedQueue newRemoteStreams = new ConcurrentLinkedQueue<>(); + // A set of listeners listening to new streams created by the peer + private final Set> streamListeners = ConcurrentHashMap.newKeySet(); + // A lock to ensure consistency between invocation of streamListeners and + // the content of the newRemoteStreams queue. + private final Lock newRemoteStreamsLock = new ReentrantLock(); + + // The connection to which the streams managed by this + // instance of QuicConnectionStreams belong to. + private final QuicConnectionImpl connection; + + // will hold the highest limit from a STREAMS_BLOCKED frame that was sent by a peer for uni + // streams. this indicates the peer isn't able to create any more uni streams, past this limit + private final AtomicLong peerUniStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit from a STREAMS_BLOCKED frame that was sent by a peer for bidi + // streams. this indicates the peer isn't able to create any more bidi streams, past this limit + private final AtomicLong peerBidiStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit at which the local endpoint couldn't create a uni stream + // and a STREAMS_BLOCKED was required to be sent. -1 indicates the local endpoint hasn't yet + // been blocked for stream creation + private final AtomicLong uniStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit at which the local endpoint couldn't create a bidi stream + // and a STREAMS_BLOCKED was required to be sent. -1 indicates the local endpoint hasn't yet + // been blocked for stream creation + private final AtomicLong bidiStreamsBlocked = new AtomicLong(-1); + // will hold the limit with which the local endpoint last sent a STREAMS_BLOCKED frame to the + // peer for uni streams. -1 indicates no STREAMS_BLOCKED frame has been sent yet. A new + // STREAMS_BLOCKED will be sent only if "uniStreamsBlocked" exceeds this + // "lastUniStreamsBlockedSent" + private final AtomicLong lastUniStreamsBlockedSent = new AtomicLong(-1); + // will hold the limit with which the local endpoint last sent a STREAMS_BLOCKED frame to the + // peer for bidi streams. -1 indicates no STREAMS_BLOCKED frame has been sent yet. A new + // STREAMS_BLOCKED will be sent only if "bidiStreamsBlocked" exceeds this + // "lastBidiStreamsBlockedSent" + private final AtomicLong lastBidiStreamsBlockedSent = new AtomicLong(-1); + // streams that have been blocked and aren't able to send data to the peer, + // due to reaching flow control limit imposed on those streams by the peer. + private final Set flowControlBlockedStreams = Collections.synchronizedSet(new HashSet<>()); + + private final Logger debug; + + // A QuicConnectionStream instance can be tied to a client connection + // or a server connection. + // If the connection is a client connection, then localFlag=0x00, + // localBidi=0x00, remoteBidi=0x01, localUni=0x02, remoteUni=0x03 + // If the connection is a server connection, then localFlag=0x01, + // localBidi=0x01, remoteBidi=0x00, localUni=0x03, remoteUni=0x02 + private final int localFlag, localBidi, remoteBidi, localUni, remoteUni; + + /** + * Creates a new instance of {@code QuicConnectionStreams} for the + * given connection. There is a 1-1 relationship between a + * {@code QuicConnectionImpl} instance and a {@code QuicConnectionStreams} + * instance. + * @param connection the connection to which the streams managed by this + * instance of {@code QuicConnectionStreams} belong. + */ + public QuicConnectionStreams(QuicConnectionImpl connection, Logger debug) { + this.connection = connection; + this.debug = Objects.requireNonNull(debug); + // implicit null check for connection + boolean isClient = connection.isClientConnection(); + localFlag = isClient ? 0 : SRV_MASK; + localBidi = isClient ? 0 : SRV_MASK; + remoteBidi = isClient ? SRV_MASK : 0; + localUni = isClient ? UNI_MASK : UNI_MASK | SRV_MASK; + remoteUni = isClient ? UNI_MASK | SRV_MASK : UNI_MASK; + sendersReady = isClient ? + new ReadyStreamQueue() : // faster stream opening + new ReadyStreamSortedQueue(); // faster stream closing + } + + /** + * {@return the next unallocated stream ID that would be expected + * for a stream of the given type} + * This method expects {@code streamType} to be a number in [0..3] but + * does not check it. An assert may be fired if an invalid type is passed. + * @param streamType The stream type, a number in [0..3] + */ + public long peekNextStreamId(int streamType) { + assert streamType >= 0 && streamType < 4; + var id = nextStreamID.get(streamType & 0x03); + return id.get(); + } + + /** + * Creates a new locally initiated unidirectional stream. + *

    + * If the stream cannot be created due to stream creation limit being reached, then this method + * will return a {@code CompletableFuture} which will complete either when the {@code timeout} + * has reached or the stream limit has been increased and the stream creation was successful. + * If the stream creation doesn't complete within the specified timeout then the returned + * {@code CompletableFuture} will complete exceptionally with a {@link QuicStreamLimitException} + * + * @param timeout the maximum duration to wait to acquire a permit for stream creation + * @return a CompletableFuture whose result on successful completion will return the newly + * created {@code QuicSenderStream} + */ + public CompletableFuture createNewLocalUniStream(final Duration timeout) { + @SuppressWarnings("unchecked") + final var streamCF = (CompletableFuture) createNewLocalStream(localUni, + StreamMode.WRITE_ONLY, timeout); + return streamCF; + } + + /** + * Creates a new locally initiated bidirectional stream. + *

    + * If the stream cannot be created due to stream creation limit being reached, then this method + * will return a {@code CompletableFuture} which will complete either when the {@code timeout} + * has reached or the stream limit has been increased and the stream creation was successful. + * If the stream creation doesn't complete within the specified timeout then the returned + * {@code CompletableFuture} will complete exceptionally with a {@link QuicStreamLimitException} + * + * @param timeout the maximum duration to wait to acquire a permit for stream creation + * @return a CompletableFuture whose result on successful completion will return the newly + * created {@code QuicBidiStream} + */ + public CompletableFuture createNewLocalBidiStream(final Duration timeout) { + @SuppressWarnings("unchecked") + final var streamCF = (CompletableFuture) createNewLocalStream(localBidi, + StreamMode.READ_WRITE, timeout); + return streamCF; + } + + private void register(long streamId, AbstractQuicStream stream) { + var previous = streams.put(streamId, stream); + assert previous == null : "stream " + streamId + " is already registered!"; + QuicTransportParameters peerParameters = connection.peerTransportParameters(); + if (peerParameters != null) { + if (debug.on()) { + debug.log("setting initial peer parameters on stream " + streamId); + } + newInitialPeerParameters(stream, peerParameters); + } + QuicTransportParameters localParameters = connection.localTransportParameters(); + if (localParameters != null) { + if (debug.on()) { + debug.log("setting initial local parameters on stream " + streamId); + } + newInitialLocalParameters(stream, localParameters); + } + if (stream instanceof QuicReceiverStream receiver && stream.isRemoteInitiated()) { + if (debug.on()) { + debug.log("accepting remote stream " + streamId); + } + newRemoteStreams.add(receiver); + acceptRemoteStreams(); + } + if (debug.on()) { + debug.log("new stream %s %s registered", streamId, stream.mode()); + } + } + + private void acceptRemoteStreams() { + newRemoteStreamsLock.lock(); + try { + for (var listener : streamListeners) { + var iterator = newRemoteStreams.iterator(); + while (iterator.hasNext()) { + var stream = iterator.next(); + if (debug.on()) { + debug.log("invoking remote stream listener for stream %s", + stream.streamId()); + } + if (listener.test(stream)) iterator.remove(); + } + } + } finally { + newRemoteStreamsLock.unlock(); + } + } + + private CompletableFuture createNewLocalStream( + final int localType, final StreamMode mode, final Duration timeout) { + assert localType >= 0 && localType < 4 : "bad local stream type " + localType; + assert (localType & SRV_MASK) == localFlag : "bad local stream type " + localType; + assert (localType & UNI_MASK) == 0 || mode == StreamMode.WRITE_ONLY + : "bad combination of local stream type (%s) and mode %s" + .formatted(localType, mode); + assert (localType & UNI_MASK) == UNI_MASK || mode == StreamMode.READ_WRITE + : "bad combination of local stream type (%s) and mode %s" + .formatted(localType, mode); + final boolean bidi = isBidirectional(localType); + final StreamCreationPermit permit = bidi ? this.localBidiMaxStreamLimit + : this.localUniMaxStreamLimit; + final CompletableFuture permitAcquisitionCF; + final long currentLimit = permit.currentLimit(); + final boolean acquired = permit.tryAcquire(); + if (acquired) { + permitAcquisitionCF = MinimalFuture.completedFuture(true); + } else { + // stream limit reached, request sending a STREAMS_BLOCKED frame + announceStreamsBlocked(bidi, currentLimit); + if (timeout.isPositive()) { + final Executor executor = this.connection.quicInstance().executor(); + if (debug.on()) { + debug.log("stream creation limit = " + permit.currentLimit() + + " reached; waiting for it to increase, timeout=" + timeout); + } + permitAcquisitionCF = permit.tryAcquire(timeout.toNanos(), NANOSECONDS, executor); + } else { + permitAcquisitionCF = MinimalFuture.completedFuture(false); + } + } + final CompletableFuture streamCF = + permitAcquisitionCF.thenCompose((acq) -> { + if (!acq) { + final String msg = "Stream limit = " + permit.currentLimit() + + " reached for locally initiated " + + (bidi ? "bidi" : "uni") + " streams"; + return MinimalFuture.failedFuture(new QuicStreamLimitException(msg)); + } + // stream limit hasn't been reached, we are allowed to create new one + final long streamId = nextStreamID.get(localType).getAndAdd(4); + final AbstractQuicStream stream = QuicStreams.createStream(connection, streamId); + assert stream.mode() == mode; + assert stream.type() == localType; + if (debug.on()) { + var strtype = (localType & UNI_MASK) == UNI_MASK ? "uni" : "bidi"; + debug.log("created new local %s stream type:%s, mode:%s, id:%s", + strtype, localType, mode, streamId); + } + register(streamId, stream); + return MinimalFuture.completedFuture(stream); + }); + return streamCF; + } + + /** + * Runs the APPLICATION space packet transmitter, if necessary, + * to potentially trigger sending a STREAMS_BLOCKED frame to the peer + * @param bidi true if the local endpoint is blocked for bidi streams, false for uni streams + * @param blockedOnLimit the stream creation limit due to which the local endpoint is + * currently blocked + */ + private void announceStreamsBlocked(final boolean bidi, final long blockedOnLimit) { + boolean runTransmitter = false; + if (bidi) { + long prevBlockedLimit = this.bidiStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.bidiStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.bidiStreamsBlocked.get(); + } + } else { + long prevBlockedLimit = this.uniStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.uniStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.uniStreamsBlocked.get(); + } + } + if (runTransmitter) { + if (debug.on()) { + debug.log("requesting packet transmission to send " + (bidi ? "bidi" : "uni") + + " STREAMS_BLOCKED with limit " + blockedOnLimit); + } + this.connection.runAppPacketSpaceTransmitter(); + } + } + + /** + * Runs the APPLICATION space packet transmitter, if necessary, to potentially trigger + * sending a MAX_STREAMS frame to the peer, upon receiving the STREAMS_BLOCKED {@code frame} + * from that peer + * @param frame the STREAMS_BLOCKED frame that was received from the peer + */ + public void peerStreamsBlocked(final StreamsBlockedFrame frame) { + final boolean bidi = frame.isBidi(); + final long blockedOnLimit = frame.maxStreams(); + boolean runTransmitter = false; + if (bidi) { + long prevBlockedLimit = this.peerBidiStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.peerBidiStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.peerBidiStreamsBlocked.get(); + } + } else { + long prevBlockedLimit = this.peerUniStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.peerUniStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.peerUniStreamsBlocked.get(); + } + } + if (runTransmitter) { + if (debug.on()) { + debug.log("requesting packet transmission in response to receiving " + + (bidi ? "bidi" : "uni") + " STREAMS_BLOCKED from peer," + + " blocked with limit " + blockedOnLimit); + } + this.connection.runAppPacketSpaceTransmitter(); + } + } + + /** + * Gets or opens a remotely initiated stream with the given stream ID. + * Creates all streams with lower IDs if needed. + * @param streamId the stream ID + * @param frameType type of the frame received, used in exceptions + * @return a remotely initiated stream with the given stream ID. + * May return null if the stream was already closed. + * @throws IllegalArgumentException if the streamID is of the wrong type for + * a remote stream. + * @throws QuicTransportException if the streamID is higher than allowed + */ + public QuicStream getOrCreateRemoteStream(long streamId, long frameType) + throws QuicTransportException { + final int streamType = streamType(streamId); + if ((streamId & SRV_MASK) == localFlag) { + throw new IllegalArgumentException("bad remote stream type %s for stream %s" + .formatted(streamType, streamId)); + } + final boolean bidi = isBidirectional(streamId); + final long maxStreamLimit = bidi ? this.remoteBidiMaxStreamLimit.get() + : this.remoteUniMaxStreamLimit.get(); + if (maxStreamLimit <= (streamId >> 2)) { + throw new QuicTransportException("stream ID %s exceeds the number of allowed streams(%s)" + .formatted(streamId, maxStreamLimit), QuicTLSEngine.KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_LIMIT_ERROR); + } + + newRemoteStreamsLock.lock(); + try { + var id = nextStreamID.get(streamType); + long nextId = id.get(); + if (nextId > streamId) { + // already created + return streams.get(streamId); + } + // id must not be modified outside newRemoteStreamsLock + long altId = id.getAndSet(streamId + 4); + assert altId == nextId : "next ID concurrently modified"; + + AbstractQuicStream stream = null; + for (long i = nextId; i <= streamId; i += 4) { + stream = QuicStreams.createStream(connection, i); + assert stream.isRemoteInitiated(); + register(i, stream); + } + assert stream != null; + assert stream.streamId() == streamId : stream.streamId(); + return stream; + } finally { + newRemoteStreamsLock.unlock(); + } + } + + + /** + * Finds a stream with the given stream ID. Returns {@code null} if no + * stream with that ID is found. + * @param streamId a stream ID + * @return the stream with the given stream ID if found, {@code null} + * otherwise. + */ + public QuicStream findStream(long streamId) { + return streams.get(streamId); + } + + /** + * Adds a listener that will be invoked when a remote stream is + * created. + * + * @apiNote The listener will be invoked with any remote streams + * already opened, and not yet acquired by another listener. + * Any stream passed to the listener is either a {@link QuicBidiStream} + * or a {@link QuicReceiverStream} depending on the + * {@linkplain QuicStreams#streamType(long) + * stream type} of the given streamId. + * The listener should return true if it wishes to acquire + * the stream. + * + * @param streamConsumer the listener + * + */ + public void addRemoteStreamListener(Predicate streamConsumer) { + newRemoteStreamsLock.lock(); + try { + streamListeners.add(streamConsumer); + acceptRemoteStreams(); + } finally { + newRemoteStreamsLock.unlock(); + } + } + + /** + * Removes a listener previously added with {@link #addRemoteStreamListener(Predicate)} + * @return {@code true} if the listener was found and removed, {@code false} otherwise + */ + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + newRemoteStreamsLock.lock(); + try { + return streamListeners.remove(streamConsumer); + } finally { + newRemoteStreamsLock.unlock(); + } + } + + /** + * {@return a stream of all currently active {@link QuicStream} in the connection} + */ + public Stream quicStreams() { + return streams.all(); + } + + /** + * {@return {@code true} if there is some data to send} + * @apiNote + * This method may return true in the case where a + * STREAM_DATA_BLOCKED frame needs to be sent, even if no + * other data is available. + */ + public boolean hasAvailableData() { + return !sendersReady.isEmpty(); + } + + /** + * {@return true if there are control frames to send} + * Typically, these are STREAMS_BLOCKED, MAX_STREAMS, RESET_STREAM, STOP_SENDING, and + * MAX_STREAM_DATA. + */ + public boolean hasControlFrames() { + return !sendersReset.isEmpty() || !receiversSend.isEmpty() + // either of these imply we may send a MAX_STREAMS frame + || peerUniStreamsBlocked.get() != -1 || peerBidiStreamsBlocked.get() != -1 + // either of these imply we should send a STREAMS_BLOCKED frame + || uniStreamsBlocked.get() > lastUniStreamsBlockedSent.get() + || bidiStreamsBlocked.get() > lastBidiStreamsBlockedSent.get(); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a stream + * that has a receiving part} + * In other words, returns {@code true} if the given stream is either + * bidirectional or peer-initiated. + * @param streamId a stream ID + */ + public boolean isReceivingStream(long streamId) { + return !isLocalUni(streamId); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a stream + * that has a sending part} + * In other words, returns {@code true} if the given stream is either + * bidirectional or local-initiated. + * @param streamId a stream ID + */ + public boolean isSendingStream(long streamId) { + return !isRemoteUni(streamId); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a local + * unidirectional stream} + * @param streamId a stream ID + */ + public boolean isLocalUni(long streamId) { + return streamType(streamId) == localUni; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a local + * bidirectional stream} + * @param streamId a stream ID + */ + public boolean isLocalBidi(long streamId) { + return streamType(streamId) == localBidi; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a + * peer initiated unidirectional stream} + * @param streamId a stream ID + */ + public boolean isRemoteUni(long streamId) { + return streamType(streamId) == remoteUni; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a + * peer initiated bidirectional stream} + * @param streamId a stream ID + */ + public boolean isRemoteBidi(long streamId) { + return streamType(streamId) == remoteBidi; + } + + /** + * Mark the stream whose ID is encoded in the given + * {@code ResetStreamFrame} as needing a RESET_STREAM frame to be sent. + * It will put the stream and the frame in the {@code sendersReset} map. + * @param streamId the id of the stream that should be reset + * @param errorCode the application error code + */ + public void requestResetStream(long streamId, long errorCode) { + assert isSendingStream(streamId); + var stream = senderImpl(streams.get(streamId)); + if (stream == null) { + if (debug.on()) { + debug.log("Can't reset stream %d: no such stream", streamId); + } + return; + } + sendersReset.putIfAbsent(stream, errorCode); + if (debug.on()) { + debug.log("Reset stream scheduled"); + } + } + + /** + * Mark the stream whose ID is encoded in the given + * {@code MaxStreamDataFrame} as needing a MAX_STREAM_DATA frame to be sent. + * It will put the stream and the frame in the {@code receiversSend} map. + * @param maxStreamDataFrame the MAX_STREAM_DATA frame to send + */ + public void requestSendMaxStreamData(MaxStreamDataFrame maxStreamDataFrame) { + Objects.requireNonNull(maxStreamDataFrame, "maxStreamDataFrame"); + long streamId = maxStreamDataFrame.streamID(); + assert isReceivingStream(streamId); + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) { + debug.log("Can't send MaxStreamDataFrame %d: no such stream", streamId); + } + return; + } + if (stream instanceof QuicReceiverStream receiver) { + // don't replace a stop sending frame, and don't replace + // a max stream data frame if it has a bigger max stream data + receiversSend.compute(receiver, (s, frame) -> { + if (frame instanceof StopSendingFrame stopSendingFrame) { + assert s.streamId() == stopSendingFrame.streamID(); + // no need to send max data frame if we are requesting + // stop sending + return frame; + } + if (frame instanceof MaxStreamDataFrame maxFrame) { + assert s.streamId() == maxFrame.streamID(); + if (maxFrame.maxStreamData() > maxStreamDataFrame.maxStreamData()) { + // send the frame that has the greater max data + return maxFrame; + } else return maxStreamDataFrame; + } + assert frame == null; + return maxStreamDataFrame; + }); + } else { + if (debug.on()) { + debug.log("Can't send %s stream %d: not a receiver stream", + maxStreamDataFrame.getClass(), streamId); + } + } + } + + + /** + * Mark the stream whose ID is encoded in the given + * {@code StopSendingFrame} as needing a STOP_SENDING frame to be sent. + * It will put the stream and the frame in the {@code receiversSend} map. + * @param stopSendingFrame the STOP_SENDING frame to send + */ + public void scheduleStopSendingFrame(StopSendingFrame stopSendingFrame) { + Objects.requireNonNull(stopSendingFrame, "stopSendingFrame"); + long streamId = stopSendingFrame.streamID(); + assert isReceivingStream(streamId); + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) { + debug.log("Can't send STOP_SENDING to stream %d: no such stream", streamId); + } + return; + } + if (stream instanceof QuicReceiverStream receiver) { + // don't need to check if we already have a frame registered: + // stop sending takes precedence. + receiversSend.put(receiver, stopSendingFrame); + } else { + if (debug.on()) { + debug.log("Can't send %s stream %d: not a receiver stream", + stopSendingFrame.getClass(), streamId); + } + } + } + + /** + * Called when the RESET_STREAM frame is acknowledged by the peer. + * @param reset the RESET_STREAM frame + */ + public void streamResetAcknowledged(ResetStreamFrame reset) { + Objects.requireNonNull(reset, "reset"); + long streamId = reset.streamId(); + assert isSendingStream(streamId) : + "stream %s is not a sending stream".formatted(streamId); + final var stream = streams.get(streamId); + if (stream == null) { + return; + } + var sender = senderImpl(stream); + if (sender != null) { + sender.resetAcknowledged(reset.finalSize()); + assert !stream.isDone() || !streams.streams.containsKey(streamId) + : "resetAcknowledged() should have removed the stream"; + if (debug.on()) { + debug.log("acknowledged reset for stream %d", streamId); + } + } + } + + /** + * Called when the final STREAM frame is acknowledged by the peer. + * @param streamFrame the final STREAM frame + */ + public void streamDataSentAcknowledged(StreamFrame streamFrame) { + long streamId = streamFrame.streamId(); + assert isSendingStream(streamId) : + "stream %s is not a sending stream".formatted(streamId); + assert streamFrame.isLast(); + final var stream = streams.get(streamId); + if (stream == null) { + return; + } + var sender = senderImpl(stream); + if (sender != null) { + sender.dataAcknowledged(streamFrame.offset() + streamFrame.dataLength()); + assert !stream.isDone() || !streams.streams.containsKey(streamId) + : "dataAcknowledged() should have removed the stream"; + if (debug.on()) { + debug.log("acknowledged data for stream %d", streamId); + } + } + } + + /** + * Tracks a stream, belonging to this connection, as being blocked from sending data + * due to flow control limit. + * + * @param streamId the stream id + */ + final void trackBlockedStream(final long streamId) { + this.flowControlBlockedStreams.add(streamId); + } + + /** + * Stops tracking a stream, belonging to this connection, that may have been previously + * tracked as being blocked due to flow control limit. + * + * @param streamId the stream id + */ + final void untrackBlockedStream(final long streamId) { + this.flowControlBlockedStreams.remove(streamId); + } + + + /** + * Removes a stream from the stream map after its state has been + * switched to DATA_RECVD or RESET_RECVD + * @param streamId the stream id + * @param stream the stream instance + */ + private void removeStream(long streamId, QuicStream stream) { + // if we were tracking this stream as blocked due to flow control, then + // stop tracking the stream. + untrackBlockedStream(streamId); + if (stream instanceof AbstractQuicStream astream) { + if (astream.isDone()) { + if (debug.on()) { + debug.log("Removing stream %d (%s)", + stream.streamId(), stream.getClass().getSimpleName()); + } + streams.remove(streamId, astream); + if (stream.isRemoteInitiated()) { + // the queue is not expected to contain many elements. + newRemoteStreams.remove(stream); + if (shouldSendMaxStreams(stream.isBidirectional())) { + this.connection.runAppPacketSpaceTransmitter(); + } + } + } else { + if (debug.on()) { + debug.log("Can't remove stream yet: %d (%s) is %s", + stream.streamId(), stream.getClass().getSimpleName(), + stream.state()); + } + } + } + assert stream instanceof AbstractQuicStream + : "stream %s: unexpected stream class: %s" + .formatted(streamId, stream.getClass()); + } + + /** + * Called when new local transport parameters are available + * @param params the new local transport parameters + */ + public void newLocalTransportParameters(final QuicTransportParameters params) { + // the limit imposed on the remote peer by the local endpoint + final long newRemoteUniMax = params.getIntParameter(ParameterId.initial_max_streams_uni); + tryIncreaseLimitTo(this.remoteUniMaxStreamLimit, newRemoteUniMax); + final long newRemoteBidiMax = params.getIntParameter(ParameterId.initial_max_streams_bidi); + tryIncreaseLimitTo(this.remoteBidiMaxStreamLimit, newRemoteBidiMax); + streams.all().forEach(s -> newInitialLocalParameters(s, params)); + } + + /** + * Called when new peer transport parameters are available + * @param params the new local transport parameters + */ + public void newPeerTransportParameters(final QuicTransportParameters params) { + // the limit imposed on the local endpoint by the remote peer + final long localUniMaxStreams = params.getIntParameter(ParameterId.initial_max_streams_uni); + if (debug.on()) { + debug.log("increasing localUniMaxStreamLimit to initial_max_streams_uni: " + + localUniMaxStreams); + } + this.localUniMaxStreamLimit.tryIncreaseLimitTo(localUniMaxStreams); + final long localBidiMaxStreams = params.getIntParameter(ParameterId.initial_max_streams_bidi); + if (debug.on()) { + debug.log("increasing localBidiMaxStreamLimit to initial_max_streams_bidi: " + + localBidiMaxStreams); + } + this.localBidiMaxStreamLimit.tryIncreaseLimitTo(localBidiMaxStreams); + // set initial parameters on streams + streams.all().forEach(s -> newInitialPeerParameters(s, params)); + if (debug.on()) { + debug.log("all streams updated (%s)", streams.streams.size()); + } + } + + /** + * Called to set initial peer parameters on a stream + * @param stream the stream on which parameters might be set + * @param params the peer transport parameters + */ + private void newInitialPeerParameters(QuicStream stream, QuicTransportParameters params) { + long streamId = stream.streamId(); + if (isLocalUni(stream.streamId())) { + if (params.isPresent(ParameterId.initial_max_stream_data_uni)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_uni); + senderImpl(stream).setMaxStreamData(maxData); + } + } else if (isLocalBidi(streamId)) { + // remote for the peer is local for us + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_remote)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_remote); + senderImpl(stream).setMaxStreamData(maxData); + } + } else if (isRemoteBidi(streamId)) { + // local for the peer is remote for us + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_local)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_local); + senderImpl(stream).setMaxStreamData(maxData); + } + } + } + + private static boolean tryIncreaseLimitTo(final AtomicLong limit, final long newLimit) { + long currentLimit = limit.get(); + while (currentLimit < newLimit) { + if (limit.compareAndSet(currentLimit, newLimit)) { + return true; + } + currentLimit = limit.get(); + } + return false; + } + + /** + * Called to set initial peer parameters on a stream + * @param stream the stream on which parameters might be set + * @param params the peer transport parameters + */ + private void newInitialLocalParameters(QuicStream stream, QuicTransportParameters params) { + long streamId = stream.streamId(); + if (isRemoteUni(stream.streamId())) { + if (params.isPresent(ParameterId.initial_max_stream_data_uni)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_uni); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } else if (isLocalBidi(streamId)) { + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_local)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_local); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } else if (isRemoteBidi(streamId)) { + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_remote)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_remote); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } + } + + /** + * Set max stream data for a stream. + * Called when a {@link jdk.internal.net.http.quic.frames.MaxStreamDataFrame + * MaxStreamDataFrame} is received. + * @param stream the stream + * @param maxStreamData the max data that the peer is willing to accept on this stream + */ + public void setMaxStreamData(QuicSenderStream stream, long maxStreamData) { + var sender = senderImpl(stream); + if (sender != null) { + final long newFinalizedLimit = sender.setMaxStreamData(maxStreamData); + // if the connection was tracking this stream as blocked due to flow control + // and if this new MAX_STREAM_DATA limit unblocked this stream, then + // stop tracking the stream. + if (newFinalizedLimit == maxStreamData) { // the proposed limit was accepted + if (!sender.isBlocked()) { + untrackBlockedStream(stream.streamId()); + } + } + } + } + + /** + * This method is called when a {@link + * jdk.internal.net.http.quic.frames.StopSendingFrame} is received + * from the peer. + * @param stream the stream for which stop sending was requested + * by the peer + * @param errorCode the error code + */ + public void stopSendingReceived(QuicSenderStream stream, long errorCode) { + var sender = senderImpl(stream); + if (sender != null) { + // if the stream was being tracked as blocked from sending data, + // due to flow control limits imposed by the peer, then we now + // stop tracking it since the peer no longer wants us to send data + // on this stream. + untrackBlockedStream(stream.streamId()); + sender.stopSendingReceived(errorCode); + } + } + + /** + * Called when the receiving part or the sending part of a stream + * reaches a terminal state. + * @param streamId the id of the stream + * @param state the terminal state + */ + public void notifyTerminalState(long streamId, StreamState state) { + assert state.isTerminal() : state; + var stream = streams.get(streamId); + if (stream != null) { + removeStream(streamId, stream); + } + } + + /** + * Called when the connection is closed by the higher level + * protocol + * @param terminationCause the termination cause + */ + public void terminate(final TerminationCause terminationCause) { + assert terminationCause != null : "termination cause is null"; + // make sure all active streams are woken up when we close a connection + streams.all().forEach((stream) -> { + if (stream instanceof QuicSenderStream) { + var sender = senderImpl(stream); + try { + sender.terminate(terminationCause); + } catch (Throwable t) { + if (debug.on()) { + debug.log("failed to close sender stream %s: %s", sender.streamId(), t); + } + } + } + if (stream instanceof QuicReceiverStream) { + var receiver = receiverImpl(stream); + try { + receiver.terminate(terminationCause); + } catch (Throwable t) { + // log and ignore + if (debug.on()) { + debug.log("failed to close receiver stream %s: %s", receiver.streamId(), t); + } + } + } + }); + } + + /** + * This method is called by when a stream has data available for sending. + * + * @param streamId the stream id of the stream which is ready + * @see QuicConnectionImpl#streamDataAvailableForSending + */ + public void enqueueForSending(long streamId) { + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) + debug.log("WARNING: stream %d not found", streamId); + return; + } + if (stream instanceof QuicSenderStream sender) { + // No need to check/assert the presence of this sender in the queue. + // In fact there is no guarantee that the sender isn't already in the + // queue, since the scheduler loop can also put it back into the queue, + // if for example, not everything that the sender wanted to send could + // fit in the quic packet. + sendersReady.add(sender); + } else { + String msg = String.format("Stream %s not a sending or bidi stream: %s", + streamId, stream.getClass().getName()); + if (debug.on()) { + debug.log("WARNING: " + msg); + } + throw new AssertionError(msg); + } + } + + /** + * If there are any streams in this connection that have been blocked from sending + * data due to flow control limit on that stream, then this method enqueues a + * {@code STREAM_DATA_BLOCKED} frame to be sent for each such stream. + */ + public final void enqueueStreamDataBlocked() { + connection.streamDataAvailableForSending(this.flowControlBlockedStreams); + } + + /** + * {@return the sender part implementation of the given stream, or {@code null}} + * This method returns null if the given stream doesn't have a sending part + * (that is, if it is a unidirectional peer initiated stream). + * @param stream a sending or bidirectional stream + */ + QuicSenderStreamImpl senderImpl(QuicStream stream) { + if (stream instanceof QuicSenderStreamImpl sender) { + return sender; + } else if (stream instanceof QuicBidiStreamImpl bidi) { + return bidi.senderPart(); + } + return null; + } + + /** + * {@return the receiver part implementation of the given stream, or {@code null}} + * This method returns null if the given stream doesn't have a receiver part + * (that is, if it is a unidirectional local initiated stream). + * @param stream a receiving or bidirectional stream + */ + QuicReceiverStreamImpl receiverImpl(QuicStream stream) { + if (stream instanceof QuicReceiverStreamImpl receiver) { + return receiver; + } else if (stream instanceof QuicBidiStreamImpl bidi) { + return bidi.receiverPart(); + } + return null; + } + + /** + * Called when a StreamFrame is received. + * @param stream the stream for which the StreamFrame was received + * @param frame the stream frame + * @throws QuicTransportException if an error occurred processing the frame + */ + public void processIncomingFrame(QuicStream stream, StreamFrame frame) throws QuicTransportException { + var receiver = receiverImpl(stream); + assert receiver != null; + receiver.processIncomingFrame(frame); + } + + /** + * Called when a ResetStreamFrame is received. + * @param stream the stream for which the ResetStreamFrame was received + * @param frame the reset stream frame + * @throws QuicTransportException if an error occurred processing the frame + */ + public void processIncomingFrame(QuicStream stream, ResetStreamFrame frame) throws QuicTransportException { + var receiver = receiverImpl(stream); + assert receiver != null; + receiver.processIncomingResetFrame(frame); + } + + public void processIncomingFrame(final QuicStream stream, final StreamDataBlockedFrame frame) { + assert stream.streamId() == frame.streamId() : "unexpected stream id " + frame.streamId() + + " in frame, expected " + stream.streamId(); + final QuicReceiverStreamImpl rcvrStream = receiverImpl(stream); + assert rcvrStream != null : "missing receiver stream for stream " + stream.streamId(); + rcvrStream.processIncomingFrame(frame); + } + + public boolean tryIncreaseStreamLimit(final MaxStreamsFrame maxStreamsFrame) { + final StreamCreationPermit permit = maxStreamsFrame.isBidi() + ? localBidiMaxStreamLimit : localUniMaxStreamLimit; + final long newLimit = maxStreamsFrame.maxStreams(); + if (debug.on()) { + if (maxStreamsFrame.isBidi()) { + debug.log("increasing localBidiMaxStreamLimit limit to: " + newLimit); + } else { + debug.log("increasing localUniMaxStreamLimit limit to: " + newLimit); + } + } + return permit.tryIncreaseLimitTo(newLimit); + } + + /** + * Checks whether any stream needs to have a STOP_SENDING, RESET_STREAM or any connection + * control frames like STREAMS_BLOCKED, MAX_STREAMS sent and adds the frame to the list + * if there's room. + * @param frames list of frames + * @param remaining maximum number of bytes that can be added by this method + * @return number of bytes actually added + */ + private long checkResetAndOtherControls(List frames, long remaining) { + if (debug.on()) + debug.log("checking reset and other control frames..."); + long added = 0; + // check STREAMS_BLOCKED, only send it if the local endpoint is blocked on a limit + // for which we haven't yet sent a STREAMS_BLOCKED + final long uniStreamsBlockedLimit = this.uniStreamsBlocked.get(); + final long lastUniStreamsBlockedSent = this.lastUniStreamsBlockedSent.get(); + if (uniStreamsBlockedLimit != -1 && uniStreamsBlockedLimit > lastUniStreamsBlockedSent) { + final StreamsBlockedFrame frame = new StreamsBlockedFrame(false, uniStreamsBlockedLimit); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Not enough space to add a STREAMS_BLOCKED frame for uni streams"); + } + } else { + frames.add(frame); + added += size; + // now that we are sending a STREAMS_BLOCKED frame, keep track of the limit + // that we sent it with + this.lastUniStreamsBlockedSent.set(frame.maxStreams()); + } + } + final long bidiStreamsBlockedLimit = this.bidiStreamsBlocked.get(); + final long lastBidiStreamsBlockedSent = this.lastBidiStreamsBlockedSent.get(); + if (bidiStreamsBlockedLimit != -1 && bidiStreamsBlockedLimit > lastBidiStreamsBlockedSent) { + final StreamsBlockedFrame frame = new StreamsBlockedFrame(true, bidiStreamsBlockedLimit); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Not enough space to add a STREAMS_BLOCKED frame for bidi streams"); + } + } else { + frames.add(frame); + added += size; + // now that we are sending a STREAMS_BLOCKED frame, keep track of the limit + // that we sent it with + this.lastBidiStreamsBlockedSent.set(frame.maxStreams()); + } + } + // check STOP_SENDING and MAX_STREAM_DATA + var rcvIterator = receiversSend.entrySet().iterator(); + while (rcvIterator.hasNext()) { + var entry = rcvIterator.next(); + var frame = entry.getValue(); + if (frame.size() > remaining - added) { + if (debug.on()) { + debug.log("Stream %s: not enough space for %s", + entry.getKey().streamId(), frame); + } + break; + } + var receiver = receiverImpl(entry.getKey()); + var size = checkSendControlFrame(receiver, frame, frames); + if (size > 0) { + added += size; + } + rcvIterator.remove(); + } + + // check RESET_STREAM + var sndIterator = sendersReset.entrySet().iterator(); + while (sndIterator.hasNext()) { + Map.Entry entry = sndIterator.next(); + var sender = senderImpl(entry.getKey()); + assert sender != null; + long finalSize = sender.dataSent(); + ResetStreamFrame frame = new ResetStreamFrame(sender.streamId(), entry.getValue(), finalSize); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Stream %s: not enough space for ResetFrame", + sender.streamId()); + } + break; + } + if (debug.on()) + debug.log("Stream %s: Adding ResetFrame", sender.streamId()); + frames.add(frame); + added += size; + sender.resetSent(); + sndIterator.remove(); + } + + if (remaining - added > 18) { + // add MAX_STREAMS if necessary + added += addMaxStreamsFrame(frames, false); + added += addMaxStreamsFrame(frames, true); + } + return added; + } + + private boolean shouldSendMaxStreams(final boolean bidi) { + final boolean rcvdStreamsBlocked = bidi + ? this.peerBidiStreamsBlocked.get() != -1 + : this.peerUniStreamsBlocked.get() != -1; + // if we either received a STREAMS_BLOCKED from the peer for that stream type + // or if our internal algorithm decides that the peer is about to reach the stream + // creation limit + return rcvdStreamsBlocked || nextMaxStreamsLimit(bidi) > 0; + } + + private long addMaxStreamsFrame(final List frames, final boolean bidi) { + final long newMaxStreamsLimit = connection.nextMaxStreamsLimit(bidi); + if (newMaxStreamsLimit == 0) { + return 0; + } + final boolean limitIncreased; + if (bidi) { + limitIncreased = tryIncreaseLimitTo(remoteBidiMaxStreamLimit, newMaxStreamsLimit); + } else { + limitIncreased = tryIncreaseLimitTo(remoteUniMaxStreamLimit, newMaxStreamsLimit); + } + if (!limitIncreased) { + return 0; + } + final MaxStreamsFrame frame = new MaxStreamsFrame(bidi, newMaxStreamsLimit); + frames.add(frame); + // now that we are sending MAX_STREAMS frame to the peer, reset the relevant + // STREAMS_BLOCKED flag that we might have set when/if we had received a STREAMS_BLOCKED + // from the peer + if (bidi) { + this.peerBidiStreamsBlocked.set(-1); + } else { + this.peerUniStreamsBlocked.set(-1); + } + if (debug.on()) { + debug.log("Increasing max remote %s streams to %s", + bidi ? "bidi" : "uni", newMaxStreamsLimit); + } + return frame.size(); + } + + public long nextMaxStreamsLimit(final boolean bidi) { + return bidi ? streams.remoteBidiNextMaxStreams : streams.remoteUniNextMaxStreams; + } + + /** + * {@return true if there are any streams on this connection which are blocked from + * sending data due to flow control limit, false otherwise} + */ + public final boolean hasBlockedStreams() { + return !this.flowControlBlockedStreams.isEmpty(); + } + + /** + * Checks whether the given stream is recorded as needing a control + * frame to be sent, and if so, add that frame to the list + * + * @param receiver the receiver part of the stream + * @param frame the frame to send + * @param frames list of frames + * @return size of the added frame, or zero if no frame was added + * @apiNote Typically, the control frame that is sent is either a MAX_STREAM_DATA + * or a STOP_SENDING frame + */ + private long checkSendControlFrame(QuicReceiverStreamImpl receiver, + QuicFrame frame, + List frames) { + if (frame == null) { + if (debug.on()) + debug.log("Stream %s: no receiver frame to send", receiver.streamId()); + return 0; + } + if (frame instanceof MaxStreamDataFrame maxStreamDataFrame) { + if (receiver.receivingState() == ReceivingStreamState.RECV) { + // if we know the final size, no point in increasing max data + if (debug.on()) + debug.log("Stream %s: Adding MaxStreamDataFrame", receiver.streamId()); + frames.add(frame); + receiver.updateMaxStreamData(maxStreamDataFrame.maxStreamData()); + return frame.size(); + } + return 0; + } else if (frame instanceof StopSendingFrame) { + if (debug.on()) + debug.log("Stream %s: Adding StopSendingFrame", receiver.streamId()); + frames.add(frame); + return frame.size(); + } else { + throw new InternalError("Should not reach here - not a control frame: " + frame); + } + } + + /** + * Package available data in {@link StreamFrame} instances and add them + * to the provided frames list. Additional frames, like connection control frames + * {@code STREAMS_BLOCKED}, {@code MAX_STREAMS} or stream flow control frames like + * {@code STREAM_DATA_BLOCKED} may also be added if space allows. The {@link StreamDataBlockedFrame} + * is added only once for a given stream, until the stream becomes ready again. + * @implSpec + * The total cumulated size of the returned frames must not exceed {@code maxSize}. + * The total cumulated lengths of the returned frames must not exceed {@code maxConnectionData}. + * + * @param encoder the {@link QuicPacketEncoder}, used if anything is quic version + * dependent. + * @param maxSize the cumulated maximum size of all the frames + * @param maxConnectionData the maximum number of stream data bytes that can + * be packaged to respect connection flow control + * constraints + * @param frames a list of frames in which to add the packaged data + * @return the total number of stream data bytes packaged in the created + * frames. This will not exceed the given {@code maxConnectionData}. + */ + public long produceFramesToSend(QuicPacketEncoder encoder, long maxSize, + long maxConnectionData, List frames) + throws QuicTransportException { + long remaining = maxSize; + long produced = 0; + try { + remaining -= checkResetAndOtherControls(frames, remaining); + // scan the streams and compose a list of frames - possibly including + // stream data blocked frames, + QuicSenderStreamImpl sender; + NEXT_STREAM: while ((sender = senderImpl(sendersReady.poll())) != null) { + long streamId = sender.streamId(); + boolean stillReady = true; + try { + do { + if (remaining == 0 || maxConnectionData == 0) break; + var state = sender.sendingState(); + switch (state) { + case SEND -> { + long offset = sender.dataSent(); + int headerSize = StreamFrame.headerSize(encoder, streamId, offset, remaining); + if (headerSize >= remaining) { + break NEXT_STREAM; + } + long maxControlled = Math.min(maxConnectionData, remaining - headerSize); + int maxData = (int) Math.min(Integer.MAX_VALUE, maxControlled); + if (maxData <= 0) { + break NEXT_STREAM; + } + ByteBuffer buffer = sender.poll(maxData); + if (buffer != null) { + int length = buffer.remaining(); + assert length <= remaining; + assert length <= maxData; + long streamSize = sender.streamSize(); + boolean fin = streamSize >= 0 && streamSize == offset + length; + if (fin) { + stillReady = false; + } + if (length > 0 || fin) { + StreamFrame frame = new StreamFrame(streamId, offset, length, fin, buffer); + int size = frame.size(); + assert size <= remaining : "stream:%s: size %s > remaining %s" + .formatted(streamId, size, remaining); + if (debug.on()) { + debug.log("stream:%s Adding StreamFrame: %s", + streamId, frame); + } + frames.add(frame); + remaining -= size; + produced += length; + maxConnectionData -= length; + } + } + var blocked = sender.isBlocked(); + if (blocked) { + // track this stream as blocked due to flow control + trackBlockedStream(streamId); + final var dataBlocked = new StreamDataBlockedFrame(streamId, sender.dataSent()); + // This might produce multiple StreamDataBlocked frames + // if the stream was added to sendersReady multiple times, so + // we check before actually sending a STREAM_DATA_BLOCKED frame + if (!frames.contains(dataBlocked)) { + var fdbSize = dataBlocked.size(); + if (dataBlocked.size() > remaining) { + // keep the stream in the ready list if we haven't been + // able to generate the StreamDataBlockedFrame + break NEXT_STREAM; + } + if (debug.on()) { + debug.log("stream:" + streamId + " sender is blocked: " + dataBlocked); + } + frames.add(dataBlocked); + remaining -= fdbSize; + } + stillReady = false; + continue NEXT_STREAM; + } + if (buffer == null) { + stillReady = sender.available() != 0; + continue NEXT_STREAM; + } + } + case DATA_SENT, DATA_RECVD, RESET_SENT, RESET_RECVD -> { + stillReady = false; + continue NEXT_STREAM; + } + case READY -> { + String msg = "stream:%s: illegal state %s".formatted(streamId, state); + throw new IllegalStateException(msg); + } + } + if (debug.on()) { + debug.log("packageStreamData: stream:%s, remaining:%s, " + + "maxConnectionData: %s, produced:%s", + streamId, remaining, maxConnectionData, produced); + } + } while (remaining > 0 && maxConnectionData > 0); + } catch (RuntimeException | AssertionError x) { + stillReady = false; + throw new QuicTransportException("Failed to compose frames for stream " + streamId, + KeySpace.ONE_RTT, 0, QuicTransportErrors.INTERNAL_ERROR.code(), x); + } finally { + if (stillReady) { + if (debug.on()) + debug.log("stream:%s is still ready", streamId); + enqueueForSending(streamId); + } else { + if (debug.on()) + debug.log("stream:%s is no longer ready", streamId); + } + } + assert maxConnectionData >= 0 : "produced " + produced + " max is " + maxConnectionData; + if (remaining == 0 || maxConnectionData == 0) break; + } + } catch (RuntimeException | AssertionError x) { + if (debug.on()) debug.log("Failed to compose frames", x); + if (Log.errors()) { + Log.logError(connection.logTag() + + ": Failed to compose frames", x); + } + throw new QuicTransportException("Failed to compose frames", + KeySpace.ONE_RTT, 0, QuicTransportErrors.INTERNAL_ERROR.code(), x); + } + return produced; + } + + private interface ReadyStreamCollection { + boolean isEmpty(); + + void add(QuicSenderStream sender); + + QuicStream poll(); + } + //This queue is used to ensure fair sending of stream data: the packageStreamData method + // will pop and push streams from/to this queue in a round-robin fashion so that one stream + // doesn't starve all the others. + private static class ReadyStreamQueue implements ReadyStreamCollection { + private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public void add(QuicSenderStream sender) { + queue.add(sender); + } + + public QuicStream poll() { + return queue.poll(); + } + } + // This queue is used to ensure fast closing of streams: it always returns + // the ready stream with the lowest ID. + private static class ReadyStreamSortedQueue implements ReadyStreamCollection { + private ConcurrentSkipListMap queue = new ConcurrentSkipListMap<>(); + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public void add(QuicSenderStream sender) { + queue.put(sender.streamId(), sender); + } + + public QuicStream poll() { + Map.Entry entry = queue.pollFirstEntry(); + if (entry == null) return null; + return entry.getValue(); + } + } + + // provides a limited view/operations over a ConcurrentHashMap(). we compute additional + // state in the remove() and put() APIs. providing only a limited set of APIs allows us + // to keep the places where we do that additional state computation, to minimal. + private final class StreamsContainer { + // A map of + private final ConcurrentMap streams = new ConcurrentHashMap<>(); + // active remote bidi stream count + private final AtomicLong remoteBidiActiveStreams = new AtomicLong(); + // active remote uni stream count + private final AtomicLong remoteUniActiveStreams = new AtomicLong(); + + private volatile long remoteBidiNextMaxStreams; + private volatile long remoteUniNextMaxStreams; + + AbstractQuicStream get(final long streamId) { + return streams.get(streamId); + } + + boolean remove(final long streamId, final AbstractQuicStream stream) { + if (!streams.remove(streamId, stream)) { + return false; + } + final int streamType = (int) (stream.streamId() & TYPE_MASK); + if (streamType == remoteBidi) { + final long currentActive = remoteBidiActiveStreams.decrementAndGet(); + remoteBidiNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteBidiMaxStreamLimit.get()); + } else if (streamType == remoteUni) { + final long currentActive = remoteUniActiveStreams.decrementAndGet(); + remoteUniNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteUniMaxStreamLimit.get()); + } + return true; + } + + AbstractQuicStream put(final long streamId, final AbstractQuicStream stream) { + final AbstractQuicStream previous = streams.put(streamId, stream); + final int streamType = (int) (stream.streamId() & TYPE_MASK); + if (streamType == remoteBidi) { + final long currentActive = remoteBidiActiveStreams.incrementAndGet(); + remoteBidiNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteBidiMaxStreamLimit.get()); + } else if (streamType == remoteUni) { + final long currentActive = remoteUniActiveStreams.incrementAndGet(); + remoteUniNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteUniMaxStreamLimit.get()); + } + return previous; + } + + Stream all() { + return streams.values().stream(); + } + + /** + * Returns the next (higher) max streams limit that can be advertised to the remote peer. + * Returns {@code 0} if the limit should not be increased. + */ + private long computeNextMaxStreamsLimit( + final int streamType, final long currentActiveCount, + final long currentMaxStreamsLimit) { + // we only deal with remote bidi or remote uni + assert (streamType == remoteBidi || streamType == remoteUni) + : "stream type is neither remote bidi nor remote uni: " + streamType; + final long usedRemoteStreams = peekNextStreamId(streamType) >> 2; + final boolean bidi = streamType == remoteBidi; + final var desiredStreamCount = bidi ? MAX_BIDI_STREAMS_WINDOW_SIZE + : MAX_UNI_STREAMS_WINDOW_SIZE; + final long desiredMaxStreams = usedRemoteStreams - currentActiveCount + desiredStreamCount; + // we compute a new limit after we consumed 25% (arbitrary decision) of the desired window + if (desiredMaxStreams - currentMaxStreamsLimit > desiredStreamCount >> 2) { + return desiredMaxStreams; + } + return 0; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java new file mode 100644 index 00000000000..1a20cbe5211 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * An interface that represents the receiving part of a stream. + *

    From RFC 9000: + * + * On the receiving part of a stream, an application protocol can: + *

      + *
    • read data; and
    • + *
    • abort reading of the stream and request closure, possibly + * resulting in a STOP_SENDING frame (Section 19.5).
    • + *
    + * + */ +public non-sealed interface QuicReceiverStream extends QuicStream { + + /** + * An enum that models the state of the receiving part of a stream. + */ + enum ReceivingStreamState implements QuicStream.StreamState { + /** + * The initial state for the receiving part of a + * stream is "Recv". + *

    + * In the "Recv" state, the endpoint receives STREAM + * and STREAM_DATA_BLOCKED frames. Incoming data is buffered + * and can be reassembled into the correct order for delivery + * to the application. As data is consumed by the application + * and buffer space becomes available, the endpoint sends + * MAX_STREAM_DATA frames to allow the peer to send more data. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RECV, + /** + * When a STREAM frame with a FIN bit is received, the final size of + * the stream is known; see Section 4.5. The receiving part of the + * stream then enters the "Size Known" state. In this state, the + * endpoint no longer needs to send MAX_STREAM_DATA frames; it only + * receives any retransmissions of stream data. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + SIZE_KNOWN, + /** + * Once all data for the stream has been received, the receiving part + * enters the "Data Recvd" state. This might happen as a result of + * receiving the same STREAM frame that causes the transition to + * "Size Known". After all data has been received, any STREAM or + * STREAM_DATA_BLOCKED frames for the stream can be discarded. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_RECVD, + /** + * The "Data Recvd" state persists until stream data has been delivered + * to the application. Once stream data has been delivered, the stream + * enters the "Data Read" state, which is a terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_READ, + /** + * Receiving a RESET_STREAM frame in the "Recv" or "Size Known" state + * causes the stream to enter the "Reset Recvd" state. This might + * cause the delivery of stream data to the application to be + * interrupted. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_RECVD, + /** + * Once the application receives the signal indicating that the + * stream was reset, the receiving part of the stream transitions to + * the "Reset Read" state, which is a terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_READ; + + @Override + public boolean isTerminal() { + return this == DATA_READ || this == RESET_READ; + } + + /** + * {@return true if this state indicates that the stream has been reset by the sender} + */ + public boolean isReset() { return this == RESET_RECVD || this == RESET_READ; } + } + + /** + * {@return the receiving state of the stream} + */ + ReceivingStreamState receivingState(); + + /** + * Connects an {@linkplain QuicStreamReader#started() unstarted} reader + * to the receiver end of this stream. + * @param scheduler A sequential scheduler that will be invoked + * when the reader is started and new data becomes available for reading + * @return a {@code QuicStreamReader} to read data from this + * stream. + * @throws IllegalStateException if a reader is already connected. + */ + QuicStreamReader connectReader(SequentialScheduler scheduler); + + /** + * Disconnect the reader, so that a new reader can be connected. + * + * @apiNote + * This can be useful for handing the stream over after having read + * or peeked at some bytes. + * + * @param reader the reader to be disconnected + * @throws IllegalStateException if the given reader is not currently + * connected to the stream + */ + void disconnectReader(QuicStreamReader reader); + + /** + * Cancels the reading side of this stream by sending + * a STOP_SENDING frame. + * + * @param errorCode the application error code + * + */ + void requestStopSending(long errorCode); + + /** + * {@return the amount of data that has been received so far} + * @apiNote This may include data that has not been read by the + * application yet, but does not count any data that may have + * been received twice. + */ + long dataReceived(); + + /** + * {@return the maximum amount of data that can be received on + * this stream} + * + * @apiNote This corresponds to the maximum amount of data that + * the peer has been allowed to send. + */ + long maxStreamData(); + + /** + * {@return the error code for this stream, or {@code -1}} + */ + long rcvErrorCode(); + + default boolean isStopSendingRequested() { return false; } + + @Override + default boolean hasError() { + return rcvErrorCode() >= 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java new file mode 100644 index 00000000000..120a9bffd5b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java @@ -0,0 +1,942 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.OrderedFlow.StreamDataFlow; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static jdk.internal.net.http.quic.QuicConnectionImpl.DEFAULT_INITIAL_STREAM_MAX_DATA; +import static jdk.internal.net.http.quic.frames.QuicFrame.MAX_VL_INTEGER; +import static jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState.*; + +/** + * A class that implements the receiver part of a quic stream. + */ +final class QuicReceiverStreamImpl extends AbstractQuicStream implements QuicReceiverStream { + + private static final int MAX_SMALL_FRAGMENTS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxSmallFragments", 100); + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final String dbgTag; + + // The dataFlow reorders incoming stream frames and removes duplicates. + // It contains frames that cannot be delivered yet because they are not + // at the expected offset. + private final StreamDataFlow dataFlow = new StreamDataFlow(); + // The orderedQueue contains frames that can be delivered to the application now. + // They are inserted in the queue in order. + // The QuicStreamReader's scheduler loop consumes this queue. + private final ConcurrentLinkedQueue orderedQueue = new ConcurrentLinkedQueue<>(); + // Desired buffer size; used when updating maxStreamData + private final long desiredBufferSize; + // Maximum stream data + private volatile long maxStreamData; + // how much data has been processed on this stream. + // This is data that was poll'ed from orderedQueue or dropped after stream reset. + private volatile long processed; + // how much data has been delivered to orderedQueue. This doesn't take into account + // frames that may be stored in the dataFlow. + private volatile long received; + // maximum of offset+length across all received frames + private volatile long maxReceivedData; + // the size of the stream, when known. Defaults to 0 when unknown. + private volatile long knownSize; + // the connected reader + private volatile QuicStreamReaderImpl reader; + // eof when the last payload has been polled by the application + private volatile boolean eof; + // the state of the receiving stream + private volatile ReceivingStreamState receivingState; + private volatile boolean requestedStopSending; + private volatile long errorCode; + + private final static long MIN_BUFFER_SIZE = 16L << 10; + QuicReceiverStreamImpl(QuicConnectionImpl connection, long streamId) { + super(connection, validateStreamId(connection, streamId)); + errorCode = -1; + receivingState = ReceivingStreamState.RECV; + dbgTag = connection.streamDbgTag(streamId, "R"); + long bufsize = DEFAULT_INITIAL_STREAM_MAX_DATA; + desiredBufferSize = Math.clamp(bufsize, MIN_BUFFER_SIZE, MAX_VL_INTEGER); + } + + private static long validateStreamId(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return streamId; + if (connection.isClientConnection() == QuicStreams.isClientInitiated(streamId)) { + throw new IllegalArgumentException("A locally initiated stream can't be read-only"); + } + return streamId; + } + + /** + * Sends a {@link ConnectionCloseFrame} due to MAX_STREAM_DATA exceeded + * for the stream. + * @param streamFrame the stream frame that caused the excess + * @param maxData the value of MAX_STREAM_DATA which was exceeded + */ + private static QuicTransportException streamControlOverflow(StreamFrame streamFrame, long maxData) throws QuicTransportException { + String reason = "Stream max data exceeded: offset=%s, length=%s, max stream data=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), maxData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FLOW_CONTROL_ERROR); + } + + // debug tag for debug logger + String dbgTag() { + return dbgTag; + } + + @Override + public StreamState state() { + return receivingState(); + } + + @Override + public ReceivingStreamState receivingState() { + return receivingState; + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + var reader = this.reader; + if (reader == null) { + reader = new QuicStreamReaderImpl(scheduler); + if (Handles.READER.compareAndSet(this, null, reader)) { + if (debug.on()) debug.log("reader connected"); + return reader; + } + } + throw new IllegalStateException("reader already connected"); + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + var previous = this.reader; + if (reader == previous) { + if (Handles.READER.compareAndSet(this, reader, null)) { + if (debug.on()) debug.log("reader disconnected"); + return; + } + } + throw new IllegalStateException("reader not connected"); + } + + @Override + public boolean isStopSendingRequested() { + return requestedStopSending; + } + + @Override + public void requestStopSending(final long errorCode) { + if (Handles.STOP_SENDING.compareAndSet(this, false, true)) { + assert requestedStopSending : "requestedStopSending should be true!"; + if (debug.on()) debug.log("requestedStopSending: true"); + var state = receivingState; + try { + setErrorCode(errorCode); + switch(state) { + case RECV, SIZE_KNOWN -> { + connection().scheduleStopSendingFrame(streamId(), errorCode); + } + // otherwise do nothing + } + } finally { + // RFC-9000, section 3.5: "If an application is no longer interested in the data it is + // receiving on a stream, it can abort reading the stream and specify an application + // error code." + // So it implies that the application isn't anymore interested in receiving the data + // that has been buffered in the stream, so we drop all buffered data on this stream + if (state != RECV && state != DATA_READ) { + // we know the final size; we can remove the stream + increaseProcessedData(knownSize); + if (switchReceivingState(RESET_READ)) { + eof = false; + } + } + dataFlow.clear(); + orderedQueue.clear(); + if (debug.on()) { + debug.log("Dropped all buffered frames on stream %d after STOP_SENDING was requested" + + " with error code 0x%s", streamId(), Long.toHexString(errorCode)); + } + } + } + } + + @Override + public long dataReceived() { + return received; + } + + @Override + public long maxStreamData() { + return maxStreamData; + } + + @Override + public boolean isDone() { + return switch (receivingState()) { + case DATA_READ, DATA_RECVD, RESET_READ, RESET_RECVD -> + // everything received from peer + true; + default -> + // the stream is only half closed + false; + }; + } + + /** + * Receives a QuicFrame from the remote peer. + * + * @param resetStreamFrame the frame received + */ + void processIncomingResetFrame(final ResetStreamFrame resetStreamFrame) + throws QuicTransportException { + try { + checkUpdateState(resetStreamFrame); + if (requestedStopSending) { + increaseProcessedData(knownSize); + switchReceivingState(RESET_READ); + } + } finally { + // make sure the state is switched to reset received. + // even if we're closing the connection + switchReceivingState(RESET_RECVD); + // wakeup reader, then throw exception. + QuicStreamReaderImpl reader = this.reader; + if (reader != null) reader.wakeup(); + } + } + + void processIncomingFrame(final StreamDataBlockedFrame streamDataBlocked) { + assert streamDataBlocked.streamId() == streamId() : "unexpected stream id"; + final long peerBlockedOn = streamDataBlocked.maxStreamData(); + final long currentLimit = this.maxStreamData; + if (peerBlockedOn > currentLimit) { + // shouldn't have happened. ignore and don't increase the limit. + return; + } + // the peer has stated that the stream is blocked due to flow control limit that we have + // imposed and has requested for increasing the limit. we approve that request + // and increase the limit only if the amount of received data that we have received and + // processed on this stream is more than 1/4 of the credit window. + if (!requestedStopSending + && currentLimit - processed < (desiredBufferSize - desiredBufferSize / 4)) { + demand(desiredBufferSize); + } else { + if (debug.on()) { + debug.log("ignoring STREAM_DATA_BLOCKED frame %s," + + " since current limit %d is large enough", streamDataBlocked, currentLimit); + } + } + } + + private void demand(final long additional) { + assert additional > 0 && additional < MAX_VL_INTEGER : "invalid demand: " + additional; + var received = dataReceived(); + var maxStreamData = maxStreamData(); + + final long newMax = Math.clamp(received + additional, maxStreamData, MAX_VL_INTEGER); + if (newMax > maxStreamData) { + connection().requestSendMaxStreamData(streamId(), newMax); + updateMaxStreamData(newMax); + } + } + + /** + * Called when the connection is closed + * @param terminationCause the termination cause + */ + void terminate(final TerminationCause terminationCause) { + setErrorCode(terminationCause.getCloseCode()); + final QuicStreamReaderImpl reader = this.reader; + if (reader != null) { + reader.wakeup(); + } + } + + @Override + public long rcvErrorCode() { + return errorCode; + } + + /** + * Receives a QuicFrame from the remote peer. + * + * @param streamFrame the frame received + */ + public void processIncomingFrame(final StreamFrame streamFrame) + throws QuicTransportException { + // RFC-9000, section 3.5: "STREAM frames received after sending a STOP_SENDING frame + // are still counted toward connection and stream flow control, even though these + // frames can be discarded upon receipt." + // so we do the necessary data size checks before checking if we sent a "STOP_SENDING" + // frame + checkUpdateState(streamFrame); + final ReceivingStreamState state = receivingState; + if (debug.on()) debug.log("receivingState: " + state); + long knownSize = this.knownSize; + // RESET was read or received: drop the frame. + if (state == RESET_READ || state == RESET_RECVD) { + if (debug.on()) { + debug.log("Dropping frame on stream %d since state is %s", + streamId(), state); + } + return; + } + if (requestedStopSending) { + // drop the frame + if (debug.on()) { + debug.log("Dropping frame that was received after a STOP_SENDING" + + " frame was sent on stream %d", streamId()); + } + increaseProcessedData(maxReceivedData); + if (state != RECV) { + // we know the final size; we can remove the stream + switchReceivingState(RESET_READ); + } + return; + } + + var readyFrame = dataFlow.receive(streamFrame); + var received = this.received; + boolean needWakeup = false; + while (readyFrame != null) { + // check again - this avoids a race condition where a frame + // would be considered ready if requestStopSending had been + // called concurrently, and `receive` was called after the + // state had been switched + if (requestedStopSending) { + return; + } + assert received == readyFrame.offset() + : "data received (%s) doesn't match offset (%s)" + .formatted(received, readyFrame.offset()); + this.received = received = received + readyFrame.dataLength(); + offer(readyFrame); + needWakeup = true; + readyFrame = dataFlow.poll(); + } + if (state == SIZE_KNOWN && received == knownSize) { + if (switchReceivingState(DATA_RECVD)) { + offerEof(); + needWakeup = true; + } + } + if (needWakeup) { + var reader = this.reader; + if (reader != null) reader.wakeup(); + } else { + int numFrames = dataFlow.size(); + long numBytes = dataFlow.buffered(); + if (numFrames > MAX_SMALL_FRAGMENTS && numBytes / numFrames < 400) { + // The peer sent a large number of small fragments + // that follow a gap and can't be immediately released to the reader; + // we need to buffer them, and the memory overhead is unreasonably high. + throw new QuicTransportException("Excessive stream fragmentation", + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.frameType(), + QuicTransportErrors.INTERNAL_ERROR); + } + } + } + + /** + * Checks for error conditions: + * - max stream data errors + * - max data errors + * - final size errors + * If everything checks OK, updates counters and returns, otherwise throws. + * + * @implNote + * This method may update counters before throwing. This is OK + * because we do not expect to use them again in this case. + * @param streamFrame received stream frame + * @throws QuicTransportException if frame is invalid + */ + private void checkUpdateState(StreamFrame streamFrame) throws QuicTransportException { + long offset = streamFrame.offset(); + long length = streamFrame.dataLength(); + assert offset >= 0; + assert length >= 0; + + // check maxStreamData + long maxData = maxStreamData; + assert maxData >= 0; + long size; + try { + size = Math.addExact(offset, length); + } catch (ArithmeticException x) { + // should not happen + if (debug.on()) { + debug.log("offset + length exceeds max value", x); + } + throw streamControlOverflow(streamFrame, Long.MAX_VALUE); + } + if (size > maxData) { + throw streamControlOverflow(streamFrame, maxData); + } + ReceivingStreamState state = receivingState; + // check finalSize if known + long knownSize = this.knownSize; + assert knownSize >= 0; + if (state != RECV && size > knownSize) { + String reason = "Stream final size exceeded: offset=%s, length=%s, final size=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + // check maxData + updateMaxReceivedData(size, streamFrame.getTypeField()); + if (streamFrame.isLast()) { + // check max received data, throw if we have data beyond the (new) EOF + if (size < maxReceivedData) { + String reason = "Stream truncated: offset=%s, length=%s, max received=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), maxReceivedData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + if (state == RECV && switchReceivingState(SIZE_KNOWN)) { + this.knownSize = size; + } else { + if (size != knownSize) { + String reason = "Stream final size changed: offset=%s, length=%s, final size=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + } + } + } + + /** + * Checks for error conditions: + * - max stream data errors + * - max data errors + * - final size errors + * If everything checks OK, updates counters and returns, otherwise throws. + * + * @implNote + * This method may update counters before throwing. This is OK + * because we do not expect to use them again in this case. + * @param resetStreamFrame received reset stream frame + * @throws QuicTransportException if frame is invalid + */ + private void checkUpdateState(ResetStreamFrame resetStreamFrame) throws QuicTransportException { + // check maxStreamData + long maxData = maxStreamData; + assert maxData >= 0; + long size = resetStreamFrame.finalSize(); + long errorCode = resetStreamFrame.errorCode(); + setErrorCode(errorCode); + if (size > maxData) { + String reason = "Stream max data exceeded: finalSize=%s, max stream data=%s" + .formatted(size, maxData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FLOW_CONTROL_ERROR); + } + ReceivingStreamState state = receivingState; + updateMaxReceivedData(size, resetStreamFrame.getTypeField()); + // check max received data, throw if we have data beyond the (new) EOF + if (size < maxReceivedData) { + String reason = "Stream truncated: finalSize=%s, max received=%s" + .formatted(size, maxReceivedData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + if (state == RECV && switchReceivingState(RESET_RECVD)) { + this.knownSize = size; + } else { + if (state == SIZE_KNOWN) { + switchReceivingState(RESET_RECVD); + } + if (size != knownSize) { + String reason = "Stream final size changed: new finalSize=%s, old final size=%s" + .formatted(size, knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + } + } + + void checkOpened() throws IOException { + final TerminationCause terminationCause = connection().terminationCause(); + if (terminationCause == null) { + return; + } + throw terminationCause.getCloseCause(); + } + + private void offer(StreamFrame frame) { + var payload = frame.payload(); + if (payload.hasRemaining()) { + orderedQueue.add(payload.slice()); + } + } + + private void offerEof() { + orderedQueue.add(QuicStreamReader.EOF); + } + + /** + * Update the value of MAX_STREAM_DATA for this stream + * @param newMaxStreamData + */ + public void updateMaxStreamData(long newMaxStreamData) { + long maxStreamData = this.maxStreamData; + boolean updated = false; + while (maxStreamData < newMaxStreamData) { + if (updated = Handles.MAX_STREAM_DATA.compareAndSet(this, maxStreamData, newMaxStreamData)) break; + maxStreamData = this.maxStreamData; + } + if (updated) { + if (debug.on()) { + debug.log("updateMaxStreamData: max stream data updated from %s to %s", + maxStreamData, newMaxStreamData); + } + } + } + + /** + * Update the {@code maxReceivedData} value, and return the amount + * by which {@code maxReceivedData} was increased. This method is a + * no-op and returns 0 if {@code maxReceivedData >= newMax}. + * + * @param newMax the new max offset - typically obtained + * by adding the length of a frame to its + * offset + * @param frameType type of frame received + * @throws QuicTransportException if flow control was violated + */ + private void updateMaxReceivedData(long newMax, long frameType) throws QuicTransportException { + assert newMax >= 0; + var max = this.maxReceivedData; + while (max < newMax) { + if (Handles.MAX_RECEIVED_DATA.compareAndSet(this, max, newMax)) { + // report accepted data to connection flow control, + // and update the amount of data received in the + // connection. This will also check whether connection + // flow control is exceeded, and throw in + // this case + connection().increaseReceivedData(newMax - max, frameType); + return; + } + max = this.maxReceivedData; + } + } + + /** + * Notifies the connection about received data that is no longer buffered. + */ + private void increaseProcessedDataBy(int diff) { + assert diff >= 0; + if (diff <= 0) return; + synchronized (this) { + if (requestedStopSending) { + // once we request stop sending, updates are handled by increaseProcessedData + return; + } + assert processed + diff <= received : processed+"+"+diff+">"+received+"("+maxReceivedData+")"; + processed += diff; + } + connection().increaseProcessedData(diff); + } + + /** + * Notifies the connection about received data that is no longer buffered. + */ + private void increaseProcessedData(long newProcessed) { + long diff; + synchronized (this) { + if (newProcessed > processed) { + diff = newProcessed - processed; + processed = newProcessed; + } else { + diff = 0; + } + } + if (diff > 0) { + connection().increaseProcessedData(diff); + } + } + + // private implementation of a QuicStreamReader for this stream + private final class QuicStreamReaderImpl extends QuicStreamReader { + + static final int STARTED = 1; + static final int PENDING = 2; + // should not need volatile here, as long as we + // switch to using synchronize whenever state & STARTED == 0 + // Once state & STARTED != 0 the state should no longer change + private int state; + + QuicStreamReaderImpl(SequentialScheduler scheduler) { + super(scheduler); + } + + @Override + public ReceivingStreamState receivingState() { + checkConnected(); + return QuicReceiverStreamImpl.this.receivingState(); + } + + @Override + public ByteBuffer poll() throws IOException { + checkConnected(); + var buffer = orderedQueue.poll(); + if (buffer == null) { + if (eof) return EOF; + var state = receivingState; + if (state == RESET_RECVD) { + increaseProcessedData(knownSize); + } + checkReset(); + // unfulfilled = maxStreamData - received; + // if we have received more than 1/4 of the buffer, update maxStreamData + if (!requestedStopSending && unfulfilled() < desiredBufferSize - desiredBufferSize / 4) { + demand(desiredBufferSize); + } + return null; + } + + if (requestedStopSending) { + // check reset again + checkReset(); + return null; + } + increaseProcessedDataBy(buffer.capacity()); + if (buffer == EOF) { + eof = true; + assert processed == received : processed + "!=" + received; + switchReceivingState(DATA_READ); + return EOF; + } + // if the amount of received data that has been processed on this stream is + // more than 1/4 of the credit window then send a MaxStreamData frame. + if (!requestedStopSending && maxStreamData - processed < desiredBufferSize - desiredBufferSize / 4) { + demand(desiredBufferSize); + } + return buffer; + } + + /** + * Checks whether the stream was reset and throws an exception if + * yes. + * + * @throws IOException if the stream is reset + */ + private void checkReset() throws IOException { + var state = receivingState; + if (state == RESET_READ || state == RESET_RECVD) { + if (state == RESET_RECVD) { + switchReceivingState(RESET_READ); + } + if (requestedStopSending) { + throw new IOException("Stream %s closed".formatted(streamId())); + } else { + throw new IOException("Stream %s reset by peer".formatted(streamId())); + } + } + checkOpened(); + } + + @Override + public ByteBuffer peek() throws IOException { + checkConnected(); + var buffer = orderedQueue.peek(); + if (buffer == null) { + checkReset(); + return eof ? EOF : null; + } + return buffer; + } + + private long unfulfilled() { + // TODO: should we synchronize to ensure consistency? + var max = maxStreamData; + var rcved = received; + return max - rcved; + } + + @Override + public QuicReceiverStream stream() { + var stream = QuicReceiverStreamImpl.this; + var reader = stream.reader; + return reader == this ? stream : null; + } + + @Override + public boolean connected() { + var reader = QuicReceiverStreamImpl.this.reader; + return reader == this; + } + + @Override + public boolean started() { + int state = this.state; + if ((state & STARTED) == STARTED) return true; + synchronized (this) { + state = this.state; + return (state & STARTED) == STARTED; + } + } + + private boolean wakeupOnStart(int state) { + assert Thread.holdsLock(this); + return (state & PENDING) != 0 + || !orderedQueue.isEmpty() + || receivingState != RECV; + } + + @Override + public void start() { + // Run the scheduler if woken up before starting + int state = this.state; + if ((state & STARTED) == 0) { + boolean wakeup = false; + synchronized (this) { + state = this.state; + if ((state & STARTED) == 0) { + wakeup = wakeupOnStart(state); + state = this.state = STARTED; + } + } + assert started(); + if (debug.on()) { + debug.log("reader started (wakeup: %s)", wakeup); + } + if (wakeup || !orderedQueue.isEmpty() || receivingState != RECV) wakeup(); + } + assert started(); + } + + private void checkConnected() { + if (!connected()) throw new IllegalStateException("reader not connected"); + } + + void wakeup() { + // Only run the scheduler after the reader is started. + int state = this.state; + boolean notstarted, pending = false; + if (notstarted = ((state & STARTED) == 0)) { + synchronized (this) { + state = this.state; + if (notstarted = ((state & STARTED) == 0)) { + state = this.state = (state | PENDING); + pending = (state & PENDING) == PENDING; + assert !started(); + } + } + } + if (notstarted) { + if (debug.on()) { + debug.log("reader not started (pending: %s)", pending); + } + return; + } + assert started(); + scheduler.runOrSchedule(connection().quicInstance().executor()); + } + } + + /** + * Called when a state change is needed + * @param newState the new state. + */ + private boolean switchReceivingState(ReceivingStreamState newState) { + ReceivingStreamState oldState = receivingState; + if (debug.on()) { + debug.log("switchReceivingState %s -> %s", + oldState, newState); + } + boolean switched = switch(newState) { + case SIZE_KNOWN -> markSizeKnown(); + case DATA_RECVD -> markDataRecvd(); + case RESET_RECVD -> markResetRecvd(); + case RESET_READ -> markResetRead(); + case DATA_READ -> markDataRead(); + default -> throw new UnsupportedOperationException("switch state to " + newState); + }; + if (debug.on()) { + if (switched) { + debug.log("switched receiving state from %s to %s", oldState, newState); + } else { + debug.log("receiving state not switched; state is %s", receivingState); + } + } + + if (switched && newState.isTerminal()) { + notifyTerminalState(newState); + } + + return switched; + } + + private void notifyTerminalState(ReceivingStreamState state) { + assert state == DATA_READ || state == RESET_READ : state; + connection().notifyTerminalState(streamId(), state); + } + + // DATA_RECV is reached when the last frame is received, + // and there's no gap + private boolean markDataRecvd() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV, SIZE_KNOWN -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, DATA_RECVD); + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ -> true; + }; + } while (!done); + return switched; + } + + // SIZE_KNOWN is reached when a stream frame with the FIN bit is received + private boolean markSizeKnown() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, SIZE_KNOWN); + case DATA_RECVD, DATA_READ, SIZE_KNOWN, RESET_RECVD, RESET_READ -> true; + }; + } while(!done); + return switched; + } + + // RESET_RECV is reached when a RESET_STREAM frame is received + private boolean markResetRecvd() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV, SIZE_KNOWN -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, RESET_RECVD); + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ -> true; + }; + } while(!done); + return switched; + } + + // Called when the consumer has polled the last data + // DATA_READ is a terminal state + private boolean markDataRead() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case SIZE_KNOWN, DATA_RECVD, RESET_RECVD -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, DATA_READ); + case RESET_READ, DATA_READ -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, DATA_READ)); + }; + } while(!done); + return switched; + } + + // Called when the consumer has read the reset + // RESET_READ is a terminal state + private boolean markResetRead() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case SIZE_KNOWN, DATA_RECVD, RESET_RECVD -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, RESET_READ); + case RESET_READ, DATA_READ -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, RESET_READ)); + }; + } while(!done); + return switched; + } + + private void setErrorCode(long code) { + Handles.ERROR_CODE.compareAndSet(this, -1, code); + } + + private static final class Handles { + static final VarHandle READER; + static final VarHandle RECEIVING_STATE; + static final VarHandle MAX_STREAM_DATA; + static final VarHandle MAX_RECEIVED_DATA; + static final VarHandle STOP_SENDING; + static final VarHandle ERROR_CODE; + static { + try { + var lookup = MethodHandles.lookup(); + RECEIVING_STATE = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "receivingState", ReceivingStreamState.class); + READER = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "reader", QuicStreamReaderImpl.class); + MAX_STREAM_DATA = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "maxStreamData", long.class); + MAX_RECEIVED_DATA = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "maxReceivedData", long.class); + STOP_SENDING = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "requestedStopSending", boolean.class); + ERROR_CODE = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "errorCode", long.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java new file mode 100644 index 00000000000..bdd5b55ee0b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; + +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * An interface that represents the sending part of a stream. + *

    From RFC 9000: + * + * On the sending part of a stream, an application protocol can: + *

      + *
    • write data, understanding when stream flow control credit + * (Section 4.1) has successfully been reserved to send the + * written data;
    • + *
    • end the stream (clean termination), resulting in a STREAM frame + * (Section 19.8) with the FIN bit set; and
    • + *
    • reset the stream (abrupt termination), resulting in a RESET_STREAM + * frame (Section 19.4) if the stream was not already in a terminal + * state.
    • + *
    + * + */ +public non-sealed interface QuicSenderStream extends QuicStream { + + /** + * An enum that models the state of the sending part of a stream. + */ + enum SendingStreamState implements QuicStream.StreamState { + /** + * The "Ready" state represents a newly created stream that is able + * to accept data from the application. Stream data might be + * buffered in this state in preparation for sending. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + READY, + /** + * In the "Send" state, an endpoint transmits -- and retransmits as + * necessary -- stream data in STREAM frames. The endpoint respects + * the flow control limits set by its peer and continues to accept + * and process MAX_STREAM_DATA frames. An endpoint in the "Send" state + * generates STREAM_DATA_BLOCKED frames if it is blocked from sending + * by stream flow control limits (Section 4.1). + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + SEND, + /** + * After the application indicates that all stream data has been sent + * and a STREAM frame containing the FIN bit is sent, the sending part + * of the stream enters the "Data Sent" state. From this state, the + * endpoint only retransmits stream data as necessary. The endpoint + * does not need to check flow control limits or send STREAM_DATA_BLOCKED + * frames for a stream in this state. MAX_STREAM_DATA frames might be received + * until the peer receives the final stream offset. The endpoint can safely + * ignore any MAX_STREAM_DATA frames it receives from its peer for a + * stream in this state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_SENT, + /** + * From any state that is one of "Ready", "Send", or "Data Sent", an + * application can signal that it wishes to abandon transmission of + * stream data. Alternatively, an endpoint might receive a STOP_SENDING + * frame from its peer. In either case, the endpoint sends a RESET_STREAM + * frame, which causes the stream to enter the "Reset Sent" state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_SENT, + /** + * Once all stream data has been successfully acknowledged, the sending + * part of the stream enters the "Data Recvd" state, which is a + * terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_RECVD, + /** + * Once a packet containing a RESET_STREAM has been acknowledged, the + * sending part of the stream enters the "Reset Recvd" state, which + * is a terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_RECVD; + + @Override + public boolean isTerminal() { + return this == DATA_RECVD || this == RESET_RECVD; + } + + /** + * {@return true if a stream in this state can be used for sending, that is, + * if this state is either {@link #READY} or {@link #SEND}}. + */ + public boolean isSending() { return this == READY || this == SEND; } + + /** + * {@return true if this state indicates that the stream has been reset by the sender} + */ + public boolean isReset() { return this == RESET_SENT || this == RESET_RECVD; } + } + + /** + * {@return the sending state of the stream} + */ + SendingStreamState sendingState(); + + /** + * Connects a writer to the sending end of this stream. + * @param scheduler A sequential scheduler that will + * push data on the returned {@linkplain + * QuicStreamWriter#QuicStreamWriter(SequentialScheduler) + * writer}. + * @return a {@code QuicStreamWriter} to write data to this + * stream. + * @throws IllegalStateException if a writer is already connected. + */ + QuicStreamWriter connectWriter(SequentialScheduler scheduler); + + /** + * Disconnect the writer, so that a new writer can be connected. + * + * @apiNote + * This can be useful for handing the stream over after having written + * some bytes. + * + * @param writer the writer to be disconnected + * @throws IllegalStateException if the given writer is not currently + * connected to the stream + */ + public void disconnectWriter(QuicStreamWriter writer); + + /** + * Abruptly closes the writing side of a stream by sending + * a RESET_STREAM frame. + * @param errorCode the application error code + */ + void reset(long errorCode) throws IOException; + + /** + * {@return the amount of data that has been sent} + * @apiNote + * This may include data that has not been acknowledged. + */ + long dataSent(); + + /** + * {@return the error code for this stream, or {@code -1}} + */ + long sndErrorCode(); + + /** + * {@return true if STOP_SENDING was received} + */ + boolean stopSendingReceived(); + + @Override + default boolean hasError() { + return sndErrorCode() >= 0; + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java new file mode 100644 index 00000000000..292face444c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java @@ -0,0 +1,662 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.util.Set; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; + +/** + * A class that implements the sending part of a quic stream. + */ +public final class QuicSenderStreamImpl extends AbstractQuicStream implements QuicSenderStream { + private volatile SendingStreamState sendingState; + private volatile QuicStreamWriterImpl writer; + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final String dbgTag; + private final StreamWriterQueueImpl queue = new StreamWriterQueueImpl(); + private volatile long errorCode; + private volatile boolean stopSendingReceived; + + QuicSenderStreamImpl(QuicConnectionImpl connection, long streamId) { + super(connection, validateStreamId(connection, streamId)); + errorCode = -1; + sendingState = SendingStreamState.READY; + dbgTag = connection.streamDbgTag(streamId, "W"); + } + + private static long validateStreamId(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return streamId; + if (connection.isClientConnection() != QuicStreams.isClientInitiated(streamId)) { + throw new IllegalArgumentException("A remotely initiated stream can't be write-only"); + } + return streamId; + } + + String dbgTag() { + return dbgTag; + } + + @Override + public SendingStreamState sendingState() { + return sendingState; + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + var writer = this.writer; + if (writer == null) { + writer = new QuicStreamWriterImpl(scheduler); + if (Handles.WRITER.compareAndSet(this, null, writer)) { + if (debug.on()) debug.log("writer connected"); + return writer; + } + } + throw new IllegalStateException("writer already connected"); + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + var previous = this.writer; + if (writer == previous) { + if (Handles.WRITER.compareAndSet(this, writer, null)) { + if (debug.on()) debug.log("writer disconnected"); + return; + } + } + throw new IllegalStateException("reader not connected"); + } + + + @Override + public void reset(long errorCode) throws IOException { + if (debug.on()) { + debug.log("Resetting stream %s due to %s", streamId(), + connection().quicInstance().appErrorToString(errorCode)); + } + setErrorCode(errorCode); + if (switchSendingState(SendingStreamState.RESET_SENT)) { + long streamId = streamId(); + if (debug.on()) { + debug.log("Requesting to send RESET_STREAM(%d, %d)", + streamId, errorCode); + } + queue.markReset(); + if (connection().isOpen()) { + connection().requestResetStream(streamId, errorCode); + } + } + } + + @Override + public long sndErrorCode() { + return errorCode; + } + + @Override + public boolean stopSendingReceived() { + return stopSendingReceived; + } + + @Override + public long dataSent() { + // returns the amount of data that has been submitted for + // sending downstream. This will be the amount of data that + // has been consumed by the downstream consumer. + return queue.bytesConsumed(); + } + + /** + * Called to set the max stream data for this stream. + * @apiNote as per RFC 9000, any value less than the current + * max stream data is ignored + * @param newLimit the proposed new max stream data + * @return the new limit that has been finalized for max stream data. + * This new limit may or may not have been increased to the proposed {@code newLimit}. + */ + public long setMaxStreamData(final long newLimit) { + return queue.setMaxStreamData(newLimit); + } + + /** + * Called by {@link QuicConnectionStreams} after a RESET_STREAM frame + * has been sent + */ + public void resetSent() { + queue.markReset(); + queue.close(); + } + + /** + * Called when the packet containing the RESET_STREAM frame for this + * stream has been acknowledged. + * @param finalSize the final size acknowledged + * @return true if the state was switched to RESET_RECVD as a result + * of this method invocation + */ + public boolean resetAcknowledged(long finalSize) { + long queueSize = queue.bytesConsumed(); + if (switchSendingState(SendingStreamState.RESET_RECVD)) { + if (debug.on()) { + debug.log("Reset received: final: %d, processed: %d", + finalSize, queueSize); + } + if (finalSize != queueSize) { + if (Log.errors()) { + Log.logError("Stream %d: Acknowledged reset has wrong size: acked: %d, expected: %d", + streamId(), finalSize, queueSize); + } + } + return true; + } + return false; + } + + /** + * Called when the packet containing the final STREAM frame for this + * stream has been acknowledged. + * @param finalSize the final size acknowledged + * @return true if the state was switched to DATA_RECVD as a result + * of this method invocation + */ + public boolean dataAcknowledged(long finalSize) { + long queueSize = queue.bytesConsumed(); + if (switchSendingState(SendingStreamState.DATA_RECVD)) { + if (debug.on()) { + debug.log("Last data received: final: %d, processed: %d", + finalSize, queueSize); + } + if (finalSize != queueSize) { + if (Log.errors()) { + Log.logError("Stream %d: Acknowledged data has wrong size: acked: %d, expected: %d", + streamId(), finalSize, queueSize); + } + } + } + return false; + } + + /** + * Called when a STOP_SENDING frame is received from the peer + * @param errorCode the error code + */ + public void stopSendingReceived(long errorCode) { + if (queue.stopSending(errorCode)) { + stopSendingReceived = true; + setErrorCode(errorCode); + try { + if (connection().isOpen()) { + reset(errorCode); + } + } catch (IOException io) { + if (debug.on()) debug.log("Reset failed: " + io); + } finally { + QuicStreamWriterImpl writer = this.writer; + if (writer != null) writer.wakeupWriter(); + } + } + } + + /** + * Called when the connection is closed locally + * @param terminationCause the termination cause + */ + void terminate(final TerminationCause terminationCause) { + setErrorCode(terminationCause.getCloseCode()); + queue.close(); + final QuicStreamWriterImpl writer = this.writer; + if (writer != null) { + writer.wakeupWriter(); + } + } + + /** + * A concrete implementation of the {@link StreamWriterQueue} for this + * stream. + */ + private final class StreamWriterQueueImpl extends StreamWriterQueue { + @Override + protected void wakeupProducer() { + // The scheduler is provided by the producer + // to wakeup and run the producer's write loop. + var writer = QuicSenderStreamImpl.this.writer; + if (writer != null) { + writer.wakeupWriter(); + } + } + + @Override + protected Logger debug() { + return debug; + } + + @Override + protected void wakeupConsumer() { + // Notify the connection impl that either the data is available + // for writing or the stream is blocked and the peer needs to be + // made aware. The connection should + // eventually call QuicSenderStreamImpl::poll to + // get the data available for writing and package it + // in a StreamFrame or notice that the stream is blocked and send a + // STREAM_DATA_BLOCKED frame. + connection().streamDataAvailableForSending(Set.of(streamId())); + } + + @Override + protected void switchState(SendingStreamState state) { + // called to indicate a change in the stream state. + // at the moment the only expected value is DATA_SENT + assert state == SendingStreamState.DATA_SENT; + switchSendingState(state); + } + + @Override + protected long streamId() { + return QuicSenderStreamImpl.this.streamId(); + } + } + + + /** + * The stream internal implementation of a QuicStreamWriter. + * Most of the logic is implemented in the StreamWriterQueue, + * which is subclassed here to provide an implementation of its + * few abstract methods. + */ + private class QuicStreamWriterImpl extends QuicStreamWriter { + QuicStreamWriterImpl(SequentialScheduler scheduler) { + super(scheduler); + } + + void wakeupWriter() { + scheduler.runOrSchedule(connection().quicInstance().executor()); + } + + @Override + public SendingStreamState sendingState() { + checkConnected(); + return QuicSenderStreamImpl.this.sendingState(); + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + checkConnected(); + SendingStreamState state = sending(last); + switch (state) { + // this isn't atomic but it doesn't really matter since reset + // will be handled by the same thread that polls. + case READY, SEND -> { + // allow a last empty buffer to be submitted even + // if the connection is closed. That can help + // unblock the consumer side. + if (buffer != QuicStreamReader.EOF || !last) { + checkOpened(); + } + queue.submit(buffer, last); + } + case RESET_SENT, RESET_RECVD -> throw streamResetException(); + case DATA_SENT, DATA_RECVD -> throw streamClosedException(); + } + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + checkConnected(); + SendingStreamState state = sending(false); + switch (state) { + // this isn't atomic but it doesn't really matter since reset + // will be handled by the same thread that polls. + case READY, SEND -> { + checkOpened(); + queue.queue(buffer); + } + case RESET_SENT, RESET_RECVD -> throw streamResetException(); + case DATA_SENT, DATA_RECVD -> throw streamClosedException(); + } + } + + /** + * Compose an exception to throw if data is submitted after the + * stream was reset + * @return a new IOException + */ + IOException streamResetException() { + long resetByPeer = queue.resetByPeer(); + if (resetByPeer < 0) { + return new IOException("stream %s reset by peer: errorCode %s" + .formatted(streamId(), - resetByPeer - 1)); + } else { + return new IOException("stream %s has been reset".formatted(streamId())); + } + } + + /** + * Compose an exception to throw if data is submitted after the + * the final data has been sent + * @return a new IOException + */ + IOException streamClosedException() { + return new IOException("stream %s is closed - all data has been sent" + .formatted(streamId())); + } + + @Override + public long credit() { + checkConnected(); + // how much data the producer can send before + // reaching the flow control limit. Could be + // negative if the limit has been reached already. + return queue.producerCredit(); + } + + @Override + public void reset(long errorCode) throws IOException { + setErrorCode(errorCode); + checkConnected(); + QuicSenderStreamImpl.this.reset(errorCode); + } + + @Override + public QuicSenderStream stream() { + var stream = QuicSenderStreamImpl.this; + var writer = stream.writer; + return writer == this ? stream : null; + } + + @Override + public boolean connected() { + var writer = QuicSenderStreamImpl.this.writer; + return writer == this; + } + + private void checkConnected() { + if (!connected()) { + throw new IllegalStateException("writer not connected"); + } + } + } + + void checkOpened() throws IOException { + final TerminationCause terminationCause = connection().terminationCause(); + if (terminationCause == null) { + return; + } + throw terminationCause.getCloseCause(); + } + + /** + * {@return the number of bytes that are available for sending, subject + * to flow control} + * @implSpec + * This method does not return more than what flow control for this + * stream would allow at the time the method is called. + * @implNote + * If the sender part is not finished initializing the default + * implementation of this method will return 0. + */ + public long available() { + return queue.readyToSend(); + } + + /** + * Whether the sending is blocked due to flow control. + * @return {@code true} if sending is blocked due to flow control + */ + public boolean isBlocked() { + return queue.consumerBlocked(); + } + + /** + * {@return the size of this stream, if known} + * @implSpec + * This method returns {@code -1} if the size of the stream is not + * known. + */ + public long streamSize() { + return queue.streamSize(); + } + + /** + * Polls at most {@code maxBytes} from the {@link StreamWriterQueue} of + * this stream. The semantics are equivalent to that of {@link + * StreamWriterQueue#poll(int)} + * @param maxBytes the maximum number of bytes to poll for sending + * @return a ByteBuffer containing at most {@code maxBytes} remaining + * bytes. + */ + public ByteBuffer poll(int maxBytes) { + return queue.poll(maxBytes); + } + + @Override + public boolean isDone() { + return switch (sendingState()) { + case DATA_RECVD, RESET_RECVD -> + // everything acknowledged + true; + default -> + // the stream is only half closed + false; + }; + } + + @Override + public StreamState state() { + return sendingState(); + } + + /** + * Called when some data is submitted (or offered) by the + * producer. If the stream is in the READY state, this will + * switch the sending state to SEND. + * @implNote + * The parameter {@code last} is ignored at this stage. + * {@link #switchSendingState(SendingStreamState) + * switchSendingState(SendingStreamState.DATA_SENT)} will be called + * later on when the last piece of data has been pushed downstream. + * + * @param last whether there will be no further data submitted + * by the producer. + * + * @return the state before switching to SEND. + */ + private SendingStreamState sending(boolean last) { + SendingStreamState state = sendingState; + if (state == SendingStreamState.READY) { + switchSendingState(SendingStreamState.SEND); + } + return state; + } + + /** + * Called when the StreamWriterQueue implementation notifies of + * a state change. + * @param newState the new state, according to the StreamWriterQueue. + */ + private boolean switchSendingState(SendingStreamState newState) { + SendingStreamState oldState = sendingState; + if (debug.on()) { + debug.log("switchSendingState %s -> %s", + oldState, newState); + } + boolean switched = switch(newState) { + case SEND -> markSending(); + case DATA_SENT -> markDataSent(); + case DATA_RECVD -> markDataRecvd(); + case RESET_SENT -> markResetSent(); + case RESET_RECVD -> markResetRecvd(); + default -> throw new UnsupportedOperationException("switch state to " + newState); + }; + if (debug.on()) { + if (switched) { + debug.log("switched sending state from %s to %s", oldState, newState); + } else { + debug.log("sending state not switched; state is %s", sendingState); + } + } + + if (switched && newState.isTerminal()) { + notifyTerminalState(newState); + } + + return switched; + } + + private void notifyTerminalState(SendingStreamState state) { + assert state.isTerminal() : state; + connection().notifyTerminalState(streamId(), state); + } + + // SEND can only be set from the READY state + private boolean markSending() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case READY -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.SEND); + case SEND, RESET_RECVD, RESET_SENT -> true; + // there should be no further submission of data after DATA_SENT + case DATA_SENT, DATA_RECVD -> + throw new IllegalStateException(String.valueOf(oldState)); + }; + } while(!done); + return switched; + } + + // DATA_SENT can only be set from the SEND state + private boolean markDataSent() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case SEND -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.DATA_SENT); + case DATA_SENT, RESET_RECVD, RESET_SENT, DATA_RECVD -> true; + case READY -> throw new IllegalStateException(String.valueOf(oldState)); + }; + } while (!done); + return switched; + } + + // Reset can only be set in the READY, SEND, or DATA_SENT state + private boolean markResetSent() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case READY, SEND, DATA_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.RESET_SENT); + case RESET_RECVD, RESET_SENT, DATA_RECVD -> true; + }; + } while(!done); + return switched; + } + + // Called when the packet containing the last frame is acknowledged + // DATA_RECVD is a terminal state + private boolean markDataRecvd() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case DATA_SENT, RESET_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.DATA_RECVD); + case RESET_RECVD, DATA_RECVD -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, SendingStreamState.RESET_RECVD)); + }; + } while(!done); + return switched; + } + + // Called when the packet containing the reset frame is acknowledged + // RESET_RECVD is a terminal state + private boolean markResetRecvd() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case DATA_SENT, RESET_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.RESET_RECVD); + case RESET_RECVD, DATA_RECVD -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, SendingStreamState.RESET_RECVD)); + }; + } while(!done); + return switched; + } + + private void setErrorCode(long code) { + Handles.ERROR_CODE.compareAndSet(this, -1, code); + } + + // Some VarHandles to implement CAS semantics on top of plain + // volatile fields in this class. + private static class Handles { + static final VarHandle SENDING_STATE; + static final VarHandle WRITER; + static final VarHandle ERROR_CODE; + static { + Lookup lookup = MethodHandles.lookup(); + try { + SENDING_STATE = lookup.findVarHandle(QuicSenderStreamImpl.class, + "sendingState", SendingStreamState.class); + WRITER = lookup.findVarHandle(QuicSenderStreamImpl.class, + "writer", QuicStreamWriterImpl.class); + ERROR_CODE = lookup.findVarHandle(QuicSenderStreamImpl.class, + "errorCode", long.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java new file mode 100644 index 00000000000..4d99784299f --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +/** + * An interface to model a QuicStream. + * A quic stream can be either unidirectional + * or bidirectional. A unidirectional stream can + * be opened for reading or for writing. + * Concrete subclasses of {@code QuicStream} should + * implement {@link QuicSenderStream} (unidirectional {@link + * StreamMode#WRITE_ONLY} stream), or {@link QuicReceiverStream} + * (unidirectional {@link StreamMode#READ_ONLY} stream), or + * {@link QuicBidiStream} (bidirectional {@link StreamMode#READ_WRITE} stream). + */ +public sealed interface QuicStream + permits QuicSenderStream, QuicReceiverStream, QuicBidiStream, AbstractQuicStream { + + /** + * An interface that unifies the three different stream states. + * @apiNote + * This is mostly used for logging purposes, to log the + * combined state of a stream. + */ + sealed interface StreamState permits + QuicReceiverStream.ReceivingStreamState, + QuicSenderStream.SendingStreamState, + QuicBidiStream.BidiStreamState { + String name(); + + /** + * {@return true if this is a terminal state} + */ + boolean isTerminal(); + } + + /** + * The stream operation mode. + * One of {@link #READ_ONLY}, {@link #WRITE_ONLY}, or {@link #READ_WRITE}. + */ + enum StreamMode { + READ_ONLY, WRITE_ONLY, READ_WRITE; + + /** + * {@return true if this operation mode allows reading data from the stream} + */ + public boolean isReadable() { + return this != WRITE_ONLY; + } + + /** + * {@return true if this operation mode allows writing data to the stream} + */ + public boolean isWritable() { + return this != READ_ONLY; + } + } + + /** + * {@return the stream ID of this stream} + */ + long streamId(); + + /** + * {@return this stream operation mode} + * One of {@link StreamMode#READ_ONLY}, {@link StreamMode#WRITE_ONLY}, + * or {@link StreamMode#READ_WRITE}. + */ + StreamMode mode(); + + /** + * {@return whether this stream is client initiated} + */ + boolean isClientInitiated(); + + /** + * {@return whether this stream is server initiated} + */ + boolean isServerInitiated(); + + /** + * {@return whether this stream is bidirectional} + */ + boolean isBidirectional(); + + /** + * {@return true if this stream is local initiated} + */ + boolean isLocalInitiated(); + + /** + * {@return true if this stream is remote initiated} + */ + boolean isRemoteInitiated(); + + /** + * The type of this stream, as an int. This is a number between + * 0 and 3 inclusive, and corresponds to the last two lowest bits + * of the stream ID. + *

      + *
    • 0x00: bidirectional, client initiated
    • + *
    • 0x01: bidirectional, server initiated
    • + *
    • 0x02: unidirectional, client initiated
    • + *
    • 0x03: unidirectional, server initiated
    • + *
    + * @return the type of this stream, as an int + */ + int type(); + + /** + * {@return the combined stream state} + * + * @apiNote + * This is mostly used for logging purposes, to log the + * combined state of a stream. + */ + StreamState state(); + + /** + * {@return true if the stream has errors} + * For a {@linkplain QuicBidiStream bidirectional} stream, + * this method returns true if either its sending part or + * its receiving part was closed with a non-zero error code. + */ + boolean hasError(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java new file mode 100644 index 00000000000..f38a12d3d7c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState; + +/** + * An abstract class to model a reader plugged into + * a QuicStream from which data can be read + */ +public abstract class QuicStreamReader { + + /** + * A sentinel inserted into the queue after the FIN it has been received. + */ + public static final ByteBuffer EOF = ByteBuffer.wrap(new byte[0]).asReadOnlyBuffer(); + + // The scheduler to invoke when data becomes + // available. + final SequentialScheduler scheduler; + + /** + * Creates a new instance of a QuicStreamReader. + * The given scheduler will not be invoked until the reader + * is {@linkplain #start() started}. + * + * @param scheduler A sequential scheduler that will + * poll data out of this reader. + */ + public QuicStreamReader(SequentialScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * {@return the receiving state of the stream} + * + * @apiNote + * This method returns the state of the {@link QuicReceiverStream} + * to which this writer is {@linkplain + * QuicReceiverStream#connectReader(SequentialScheduler) connected}. + * + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + * + */ + public abstract ReceivingStreamState receivingState(); + + /** + * {@return the ByteBuffer at the head of the queue, + * or null if no data is available}. If the end of the stream is + * reached then {@link #EOF} is returned. + * + * @implSpec + * This method behave just like {@link Queue#poll()}. + * + * @throws IOException if the stream was closed locally or + * reset by the peer + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + */ + public abstract ByteBuffer poll() throws IOException; + + /** + * {@return the ByteBuffer at the head of the queue, + * or null if no data is available} + * + * @implSpec + * This method behave just like {@link Queue#peek()}. + * + * @throws IOException if the stream was reset by the peer + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + */ + public abstract ByteBuffer peek() throws IOException; + + /** + * {@return the stream this reader is connected to, or {@code null} + * if this reader is not currently {@linkplain #connected() connected}} + */ + public abstract QuicReceiverStream stream(); + + + /** + * {@return true if this reader is connected to its stream} + * @see QuicReceiverStream#connectReader(SequentialScheduler) + * @see QuicReceiverStream#disconnectReader(QuicStreamReader) + */ + public abstract boolean connected(); + + /** + * {@return true if this reader has been {@linkplain #start() started}} + */ + public abstract boolean started(); + + /** + * Starts the reader. The {@linkplain + * QuicReceiverStream#connectReader(SequentialScheduler) scheduler} + * will not be invoked until the reader is {@linkplain #start() started}. + */ + public abstract void start(); + + /** + * {@return whether reset was received or read by this reader} + */ + public boolean isReset() { + return stream().receivingState().isReset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java new file mode 100644 index 00000000000..ef1de558e10 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; + +/** + * An abstract class to model a writer plugged into + * a QuicStream to which data can be written. The data + * is wrapped in {@link StreamFrame} + * before being written. + */ +public abstract class QuicStreamWriter { + + // The scheduler to invoke when flow credit + // become available. + final SequentialScheduler scheduler; + + /** + * Creates a new instance of a QuicStreamWriter. + * @param scheduler A sequential scheduler that will + * push data into this writer. + */ + public QuicStreamWriter(SequentialScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * {@return the sending state of the stream} + * + * @apiNote + * This method returns the state of the {@link QuicSenderStream} + * to which this writer is {@linkplain + * QuicSenderStream#connectWriter(SequentialScheduler) connected}. + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract SendingStreamState sendingState(); + + /** + * Pushes a ByteBuffer to be scheduled for writing on the stream. + * The ByteBuffer will be wrapped in a StreamFrame before being + * sent. Data that cannot be sent due to a lack of flow + * credit will be buffered. + * + * @param buffer A byte buffer to schedule for writing + * @param last Whether that's the last data that will be sent + * through this stream. + * + * @throws IOException if the state of the stream isn't + * {@link SendingStreamState#READY} or {@link SendingStreamState#SEND} + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void scheduleForWriting(ByteBuffer buffer, boolean last) + throws IOException; + + /** + * Queues a {@code ByteBuffer} on the writing queue for this stream. + * The consumer will not be woken up. More data should be submitted + * using {@link #scheduleForWriting(ByteBuffer, boolean)} in order + * to wake the consumer. + * + * @apiNote + * Use this method as a hint that more data will be + * upcoming shortly that might be aggregated with + * the data being queued in order to reduce the number + * of packets that will be sent to the peer. + * This is useful when a small number of bytes + * need to be written to the stream before actual stream + * data. Typically, this can be used for writing the + * HTTP/3 stream type for a unidirectional HTTP/3 stream + * before starting to send stream data. + * + * @param buffer A byte buffer to schedule for writing + * + * @throws IOException if the state of the stream isn't + * {@link SendingStreamState#READY} or {@link SendingStreamState#SEND} + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void queueForWriting(ByteBuffer buffer) + throws IOException; + + /** + * Indicates how many bytes the writer is + * prepared to received for sending. + * When that value grows from 0, and if the queue has + * no pending data, the {@code scheduler} + * is triggered to elicit more calls to + * {@link #scheduleForWriting(ByteBuffer,boolean)}. + * + * @apiNote This information is used to avoid + * buffering too much data while waiting for flow + * credit on the underlying stream. When flow credit + * is available, the {@code scheduler} loop is + * invoked to resume writing. The scheduler can then + * call this method to figure out how much data to + * request from upstream. + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract long credit(); + + /** + * Abruptly resets the stream. + * + * @param errorCode the application error code + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void reset(long errorCode) throws IOException; + + /** + * {@return the stream this writer is connected to, or {@code null} + * if this writer isn't currently {@linkplain #connected() connected}} + */ + public abstract QuicSenderStream stream(); + + /** + * {@return true if this writer is connected to its stream} + * @see QuicSenderStream#connectWriter(SequentialScheduler) + * @see QuicSenderStream#disconnectWriter(QuicStreamWriter) + */ + public abstract boolean connected(); + + /** + * {@return true if STOP_SENDING was received} + */ + public boolean stopSendingReceived() { + return connected() ? stream().stopSendingReceived() : false; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java new file mode 100644 index 00000000000..8706cdcf887 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * A collection of utilities methods to analyze and work with + * quic streams. + */ +public final class QuicStreams { + private QuicStreams() { throw new InternalError("should not come here"); } + + public static final int TYPE_MASK = 0x03; + public static final int UNI_MASK = 0x02; + public static final int SRV_MASK = 0x01; + + public static int streamType(long streamId) { + return (int) streamId & TYPE_MASK; + } + + public static boolean isBidirectional(long streamId) { + return ((int) streamId & UNI_MASK) == 0; + } + + public static boolean isUnidirectional(long streamId) { + return ((int) streamId & UNI_MASK) == UNI_MASK; + } + + public static boolean isBidirectional(int streamType) { + return (streamType & UNI_MASK) == 0; + } + + public static boolean isUnidirectional(int streamType) { + return (streamType & UNI_MASK) == UNI_MASK; + } + + public static boolean isClientInitiated(long streamId) { + return ((int) streamId & SRV_MASK) == 0; + } + + public static boolean isServerInitiated(long streamId) { + return ((int) streamId & SRV_MASK) == SRV_MASK; + } + + public static boolean isClientInitiated(int streamType) { + return (streamType & SRV_MASK) == 0; + } + + public static boolean isServerInitiated(int streamType) { + return (streamType & SRV_MASK) == SRV_MASK; + } + + public static AbstractQuicStream createStream(QuicConnectionImpl connection, long streamId) { + int type = streamType(streamId); + boolean isClient = connection.isClientConnection(); + return switch (type) { + case 0x00, 0x01 -> new QuicBidiStreamImpl(connection, streamId); + case 0x02 -> isClient ? new QuicSenderStreamImpl(connection, streamId) + : new QuicReceiverStreamImpl(connection, streamId); + case 0x03 -> isClient ? new QuicReceiverStreamImpl(connection, streamId) + : new QuicSenderStreamImpl(connection, streamId); + default -> throw new IllegalArgumentException("bad stream type %s for stream %s" + .formatted(type, streamId)); + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java new file mode 100644 index 00000000000..5495681b12a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.AbstractQueuedLongSynchronizer; +import java.util.function.Function; + +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * Quic specifies limits on the number of uni and bidi streams that an endpoint can create. + * This {@code StreamCreationPermit} is used to keep track of that limit and is expected to be + * used before attempting to open a Quic stream. Either of {@link #tryAcquire()} or + * {@link #tryAcquire(long, TimeUnit, Executor)} must be used before attempting to open a new stream. Stream + * must only be opened if that method returns {@code true} which implies the stream creation limit + * hasn't yet reached. + *

    + * It is expected that for each of the stream types (remote uni, remote bidi, local uni + * and local bidi) a separate instance of {@code StreamCreationPermit} will be used. + *

    + * An instance of {@code StreamCreationPermit} starts with an initial limit and that limit can be + * increased to newer higher values whenever necessary. The limit however cannot be reduced to a + * lower value. + *

    + * None of the methods, including {@link #tryAcquire(long, TimeUnit, Executor)} and {@link #tryAcquire()} + * block the caller thread. + */ +final class StreamCreationPermit { + + private final InternalSemaphore semaphore; + private final SequentialScheduler permitAcquisitionScheduler = + SequentialScheduler.lockingScheduler(new TryAcquireTask()); + + private final ConcurrentLinkedQueue acquirers = new ConcurrentLinkedQueue<>(); + + /** + * @param initialMaxStreams the initial max streams limit + * @throws IllegalArgumentException if {@code initialMaxStreams} is less than 0 + * @throws NullPointerException if executor is null + */ + StreamCreationPermit(final long initialMaxStreams) { + if (initialMaxStreams < 0) { + throw new IllegalArgumentException("Invalid max streams limit: " + initialMaxStreams); + } + this.semaphore = new InternalSemaphore(initialMaxStreams); + } + + /** + * Attempts to increase the limit to {@code newLimit}. The limit will be atomically increased + * to the {@code newLimit}. If the {@linkplain #currentLimit() current limit} is higher than + * the {@code newLimit}, then the limit isn't changed and this method returns {@code false}. + * + * @param newLimit the new limit + * @return true if the limit was successfully increased to {@code newLimit}, false otherwise. + */ + boolean tryIncreaseLimitTo(final long newLimit) { + final boolean increased = this.semaphore.tryIncreaseLimitTo(newLimit); + if (increased) { + // let any waiting acquirers attempt acquiring a permit + permitAcquisitionScheduler.runOrSchedule(); + } + return increased; + } + + /** + * Attempts to acquire a permit to open a new stream. This method does not block and returns + * immediately. A stream should only be opened if the permit was successfully acquired. + * + * @return true if the permit was acquired and a new stream is allowed to be opened. + * false otherwise. + */ + boolean tryAcquire() { + return this.semaphore.tryAcquireShared(1) >= 0; + } + + /** + * Attempts to acquire a permit to open a new stream. If the permit is available then this method + * returns immediately with a {@link CompletableFuture} whose result is {@code true}. If the + * permit isn't currently available then this method returns a {@code CompletableFuture} which + * completes with a result of {@code false} if no permits were available for the duration + * represented by the {@code timeout}. If during this {@code timeout} period, a permit is + * acquired, because of an increase in the stream limit, then the returned + * {@code CompletableFuture} completes with a result of {@code true}. + * + * @param timeout the maximum amount of time to attempt acquiring a permit, after which the + * {@code CompletableFuture} will complete with a result of {@code false} + * @param unit the timeout unit + * @param executor the executor that will be used to asynchronously complete the + * returned {@code CompletableFuture} if a permit is acquired after this + * method has returned + * @return a {@code CompletableFuture} whose result will be {@code true} if the permit was + * acquired and {@code false} otherwise + * @throws IllegalArgumentException if {@code timeout} is negative + * @throws NullPointerException if the {@code executor} is null + */ + CompletableFuture tryAcquire(final long timeout, final TimeUnit unit, + final Executor executor) { + Objects.requireNonNull(executor); + if (timeout < 0) { + throw new IllegalArgumentException("invalid timeout: " + timeout); + } + if (tryAcquire()) { + return MinimalFuture.completedFuture(true); + } + final CompletableFuture future = new MinimalFuture() + .orTimeout(timeout, unit) + .handle((acquired, t) -> { + if (t instanceof TimeoutException te) { + // timed out + return MinimalFuture.completedFuture(false); + } + if (t == null) { + // completed normally + return MinimalFuture.completedFuture(acquired); + } + return MinimalFuture.failedFuture(t); + }).thenComposeAsync(Function.identity(), executor); + var waiter = new Waiter(future, executor); + this.acquirers.add(waiter); + // if the future completes in timeout the Waiter should be removed from the list. + // because this is a queue it might not be too efficient... + future.whenComplete((r,t) -> { if (r != null && !r) acquirers.remove(waiter);}); + // if stream limit might have increased in the meantime, + // trigger the task to have this newly registered waiter notified + // TODO: should we call runOrSchedule(executor) here instead? + permitAcquisitionScheduler.runOrSchedule(); + return future; + } + + /** + * {@return the current limit for stream creation} + */ + long currentLimit() { + return this.semaphore.currentLimit(); + } + + private final record Waiter(CompletableFuture acquirer, + Executor executor) { + Waiter { + assert acquirer != null : "Acquirer cannot be null"; + assert executor != null : "Executor cannot be null"; + } + } + + /** + * A task which iterates over the waiting acquirers and attempt + * to acquire a permit. If successful, the waiting acquirer(s) (i.e. the CompletableFuture(s)) + * are completed successfully. If not, the waiting acquirers continue to stay in the wait list + */ + private final class TryAcquireTask implements Runnable { + + @Override + public void run() { + Waiter waiter = null; + while ((waiter = acquirers.peek()) != null) { + final CompletableFuture acquirer = waiter.acquirer; + if (acquirer.isCancelled() || acquirer.isDone()) { + // no longer interested, or already completed, remove it + acquirers.remove(waiter); + continue; + } + if (!tryAcquire()) { + // limit reached, no permits available yet + break; + } + // compose a step which rolls back the acquired permit if the + // CompletableFuture completed in some other thread, after the permit was acquired. + acquirer.whenComplete((acquired, t) -> { + final boolean shouldRollback = acquirer.isCancelled() + || t != null + || !acquired; + if (shouldRollback) { + final boolean released = StreamCreationPermit.this.semaphore.releaseShared(1); + assert released : "acquired permit wasn't released"; + // an additional permit is now available due to the release, let any waiters + // acquire it if needed + permitAcquisitionScheduler.runOrSchedule(); + } + }); + // got a permit, complete the waiting acquirer + acquirers.remove(waiter); + acquirer.completeAsync(() -> true, waiter.executor); + } + } + } + + /** + * A {@link AbstractQueuedLongSynchronizer} whose {@linkplain #getState() state} represents + * the number of permits that have currently been acquired. This {@code Semaphore} only + * supports "shared" mode; i.e. exclusive mode isn't supported. + *

    + * The {@code Semaphore} maintains a {@linkplain #limit limit} which represents + * the maximum number of permits that can be acquired through an instance of this class. + * The {@code limit} can be {@linkplain #tryIncreaseLimitTo(long) increased} but cannot be + * reduced from the previous set limit. + */ + private static final class InternalSemaphore extends AbstractQueuedLongSynchronizer { + private static final long serialVersionUID = 4280985311770761500L; + + private final AtomicLong limit; + + /** + * @param initialLimit the initial limit, must be >=0 + */ + private InternalSemaphore(final long initialLimit) { + assert initialLimit >= 0 : "not a positive initial limit: " + initialLimit; + this.limit = new AtomicLong(initialLimit); + setState(0 /* num acquired */); + } + + /** + * Attempts to acquire additional permits. If no permits can be acquired, + * then this method returns -1. Upon successfully acquiring the + * {@code additionalAcquisitions} this method returns a value {@code >=0} which represents + * the additional number of permits that are available for acquisition. + * + * @param additionalAcquisitions the additional permits that are requested + * @return -1 If no permits can be acquired. Value >=0, representing the permits that are + * still available for acquisition. + */ + @Override + protected long tryAcquireShared(final long additionalAcquisitions) { + while (true) { + final long alreadyAcquired = getState(); + final long totalOnAcquisition = alreadyAcquired + additionalAcquisitions; + final long currentLimit = limit.get(); + if (totalOnAcquisition > currentLimit) { + return -1; // exceeds limit, so cannot acquire + } + final long numAvailableUponAcquisition = currentLimit - totalOnAcquisition; + if (compareAndSetState(alreadyAcquired, totalOnAcquisition)) { + return numAvailableUponAcquisition; + } + } + } + + /** + * Attempts to release permits + * + * @param releases the number of permits to release + * @return true if the permits were released, false otherwise + * @throws IllegalArgumentException if the number of {@code releases} exceeds the total + * number of permits that have been acquired + */ + @Override + protected boolean tryReleaseShared(final long releases) { + while (true) { + final long currentAcquisitions = getState(); + final long totalAfterRelease = currentAcquisitions - releases; + if (totalAfterRelease < 0) { + // we attempted to release more permits than what was acquired + throw new IllegalArgumentException("cannot release " + releases + + " permits from " + currentAcquisitions + " acquisitions"); + } + if (compareAndSetState(currentAcquisitions, totalAfterRelease)) { + return true; + } + } + } + + /** + * Tries to increase the limit to the {@code newLimit}. If the {@code newLimit} is lesser + * than the current limit, then this method returns false. Otherwise, this method will attempt + * to atomically increase the limit to {@code newLimit}. + * + * @param newLimit The new limit to set + * @return true if the limit was increased to {@code newLimit}. false otherwise + */ + private boolean tryIncreaseLimitTo(final long newLimit) { + long currentLimit = this.limit.get(); + while (currentLimit < newLimit) { + if (this.limit.compareAndSet(currentLimit, newLimit)) { + return true; + } + currentLimit = this.limit.get(); + } + return false; + } + + /** + * {@return the current limit} + */ + private long currentLimit() { + return this.limit.get(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java new file mode 100644 index 00000000000..ebf205479f6 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java @@ -0,0 +1,550 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; + +/** + * A class to handle the writing queue of a {@link QuicSenderStream}. + * This class maintains a queue of byte buffer containing stream data + * that has not yet been packaged for sending. It also keeps track of + * the max stream data value. + * It acts as a mailbox between a producer (typically a {@link QuicStreamWriter}), + * and a consumer (typically a {@link jdk.internal.net.http.quic.QuicConnectionImpl}). + * This class is abstract: a concrete implementation of this class must only + * implement {@link #wakeupProducer()} and {@link #wakeupConsumer()} which should + * wake up the producer and consumer respectively, when data can be polled or + * submitted from the queue. + */ +abstract class StreamWriterQueue { + /** + * The amount of data that a StreamWriterQueue is willing to buffer. + * The queue will buffer excess data, but will not wake up the producer + * until the excess is consumed. + */ + private static final int BUFFER_SIZE = + Utils.getIntegerProperty("jdk.httpclient.quic.streamBufferSize", 1 << 16); + + // The current buffer containing data to send. + private ByteBuffer current; + // The offset of the data that has been consumed + private volatile long bytesConsumed; + // The offset of the data that has been supplied by the + // producer. + // bytesProduced >= bytesConsumed at all times. + private volatile long bytesProduced; + // The stream size, when known, -1 otherwise. + // The stream size may be known at the creation of the stream, + // or at the latest when the last ByteBuffer is provided by + // the producer. + private volatile long streamSize = -1; + // true if reset was requested, false otherwise + private volatile boolean resetRequested; + // The maximum offset that will be accepted by the peer at this + // time. bytesConsumed <= maxStreamData at all times. + private volatile long maxStreamData; + // negative if stop sending was received; contains -(errorCode + 1) + private volatile long stopSending; + // The queue to buffer data before it's polled by the consumer + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + private final ReentrantLock lock = new ReentrantLock(); + + protected final void lock() { + lock.lock(); + } + + protected final void unlock() { + lock.unlock(); + } + + protected abstract Logger debug(); + + /** + * This method is called by the consumer to poll data from the stream + * queue. This method will return a {@code ByteBuffer} with at most + * {@code maxbytes} remaining bytes. The {@code ByteBuffer} may contain + * less bytes if not enough bytes are available, or if there is not + * enough {@linkplain #consumerCredit() credit} to send {@code maxbytes} + * to the peer. Only stream credit is taken into account. Taking into + * account connection credit is the responsibility of the caller. + * If there is no credit, or if there is no data available, {@code null} + * is returned. When credit and data are available again, {@link #wakeupConsumer()} + * is called to wake up the consumer. + * + * @apiNote + * This method increases the consumer offset. It must not be called concurrently + * by two different threads. + * + * @implNote + * If the producer was blocked due to full buffer before this method was called + * and the method removes enough of buffered data, + * {@link #wakeupProducer()} is called. + * + * @param maxbytes the maximum number of bytes the consumer is prepared + * to consume. + * @return a {@code ByteBuffer} containing at most {@code maxbytes}, or {@code null} + * if no data is available or data is blocked by flow control. + */ + public final ByteBuffer poll(int maxbytes) { + boolean producerWasBlocked, producerUnblocked; + long produced, consumed; + ByteBuffer buffer; + long credit = consumerCredit(); + assert credit >= 0 : credit; + if (credit < maxbytes) { + maxbytes = (int)credit; + } + if (maxbytes <= 0) return null; + lock(); + try { + producerWasBlocked = producerBlocked(); + buffer = current; + if (buffer == null) { + buffer = current = queue.poll(); + } + if (buffer == null) { + return null; + } + int remaining = buffer.remaining(); + int position = buffer.position(); + consumed = bytesConsumed; + if (remaining <= maxbytes) { + current = queue.poll(); + bytesConsumed = consumed = Math.addExact(consumed, remaining); + } else { + buffer = buffer.slice(position, maxbytes); + current.position(position + maxbytes); + bytesConsumed = consumed = Math.addExact(consumed, maxbytes); + } + long size = streamSize; + produced = bytesProduced; + producerUnblocked = producerWasBlocked && !producerBlocked(); + if (StreamWriterQueue.class.desiredAssertionStatus()) { + assert consumed <= produced + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + assert size == -1 || consumed <= size + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + assert size == -1 || produced <= size + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + } + if (size >= 0 && consumed == size) { + switchState(SendingStreamState.DATA_SENT); + } + } finally { + unlock(); + } + if (producerUnblocked) { + debug().log("producer unblocked produced:%s, consumed:%s", + produced, consumed); + wakeupProducer(); + } + return buffer; + } + + /** + * Updates the flow control credit for this queue. + * The maximum offset that will be accepted by the consumer + * can only increase. Value that are less or equal to the + * current value of the max stream data are ignored. + * + * @implSpec + * If the consumer was blocked due to flow control before + * this method was called, and the new value of the max + * stream data allows to unblock the consumer, and data + * is available, {@link #wakeupConsumer()} is called. + * + * @param data the maximum offset that will be accepted by + * the consumer + * @return the maximum offset that will be accepted by the + * consumer. + */ + public final long setMaxStreamData(long data) { + assert data >= 0 : "maxStreamData: " + data; + long max, produced, consumed; + boolean consumerWasBlocked, consumerUnblocked; + lock(); + try { + max = maxStreamData; + consumed = bytesConsumed; + produced = bytesProduced; + consumerWasBlocked = consumerBlocked(); + if (data <= max) return max; + maxStreamData = max = data; + consumerUnblocked = consumerWasBlocked && !consumerBlocked(); + if (StreamWriterQueue.class.desiredAssertionStatus()) { + long size = streamSize; + assert consumed <= produced; + assert size == -1 || consumed <= size; + assert size == -1 || produced <= size; + } + } finally { + unlock(); + } + debug().log("set max stream data: %s", max); + if (consumerUnblocked && produced > 0) { + debug().log("consumer unblocked produced:%s, consumed:%s, max stream data:%s", + produced, consumed, max); + wakeupConsumer(); + } + return max; + } + + /** + * Whether the producer is blocked due to flow control. + * + * @return whether the producer is blocked due to full buffers + */ + public final boolean producerBlocked() { + return producerCredit() <= 0; + } + + /** + * Whether the consumer is blocked due to flow control. + * + * @return whether the producer is blocked due to flow control + */ + public final boolean consumerBlocked() { + return consumerCredit() <= 0; + } + + /** + * {@return the offset of the data consumed by the consumer} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #poll(int)} is called concurrently + * by another thread. + */ + public final long bytesConsumed() { + return bytesConsumed; + } + + /** + * {@return the offset of the data provided by the producer} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #queue(ByteBuffer)} are called concurrently + * by another thread. + */ + public final long bytesProduced() { + return bytesProduced; + } + + /** + * {@return the amount of produced data which has not been consumed yet} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #queue(ByteBuffer)} or + * {@link #poll(int)} are called concurrently + * by another thread. + */ + public final long available() { + return bytesProduced - bytesConsumed; + } + + /** + * {@return the stream size if known, {@code -1} otherwise} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * is called concurrently by another thread. + */ + public final long streamSize() { + return streamSize; + } + + /** + * {@return the maximum offset that the peer is prepared to accept} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #setMaxStreamData(long)} is called + * concurrently by another thread. + */ + public final long maxStreamData() { + return maxStreamData; + } + + /** + * {@return {@code true} if the consumer has reached the end of + * this stream (equivalent to EOF)} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #poll(int)} are called concurrently + * by another thread. + */ + public final boolean isConsumerDone() { + long size = streamSize; + long consumed = bytesConsumed; + assert size == -1 || size >= consumed; + return size >= 0 && size <= consumed; + } + + /** + * {@return {@code true} if the producer has reached the end of + * this stream (equivalent to EOF)} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * is called concurrently by another thread. + */ + public final boolean isProducerDone() { + return streamSize >= 0; + } + + /** + * This method is called by the producer to submit data to this + * stream. The producer should not modify the provided buffer + * after this point. The provided buffer will be queued even if + * the produced data exceeds the maximum offset that the peer + * is prepared to accept. + * + * @apiNote + * If sufficient credit is available, this method will wake + * up the consumer. + * + * @param buffer a buffer containing data for the stream + * @param last whether this is the last buffer that will ever be + * provided by the provided + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + public final void submit(ByteBuffer buffer, boolean last) throws IOException { + offer(buffer, last, false); + } + + /** + * This method is called by the producer to queue data to this + * stream. The producer should not modify the provided buffer + * after this point. The provided buffer will be queued even if + * the produced data exceeds the maximum offset that the peer + * is prepared to accept. + * + * @apiNote + * The consumer will not be woken, even if enough credit is + * available. More data should be submitted using + * {@link #submit(ByteBuffer, boolean)} in order to wake up the consumer. + * + * @param buffer a buffer containing data for the stream + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + public final void queue(ByteBuffer buffer) throws IOException { + offer(buffer, false, true); + } + + /** + * Queues a buffer in the writing queue. + * + * @param buffer the buffer to queue + * @param last whether this is the last data for the stream + * @param waitForMore whether we should wait for the next submission before + * waking up the consumer + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + private void offer(ByteBuffer buffer, boolean last, boolean waitForMore) + throws IOException { + long length = buffer.remaining(); + long consumed, produced, max; + boolean wakeupConsumer; + lock(); + try { + long stopSending = this.stopSending; + if (stopSending < 0) { + throw new IOException("Stream %s reset by peer: errorCode %s" + .formatted(streamId(), 1 - stopSending)); + } + if (resetRequested) return; + if (streamSize >= 0) { + throw new IllegalStateException("Too many bytes provided"); + } + consumed = bytesConsumed; + max = maxStreamData; + produced = Math.addExact(bytesProduced, length); + bytesProduced = produced; + if (length > 0 || last) { + // allow to queue a zero-length buffer if it's the last. + queue.offer(buffer); + } + if (last) { + streamSize = produced; + } + assert consumed <= produced; + wakeupConsumer = consumed < max && consumed < produced + || consumed == produced && last; + } finally { + unlock(); + } + if (wakeupConsumer && !waitForMore) { + debug().log("consumer unblocked produced:%s, consumed:%s, max stream data:%s", + produced, consumed, max); + wakeupConsumer(); + } + } + + /** + * {@return the credit of the producer} + * @implSpec + * this is the desired buffer size minus the amount of data already buffered. + */ + public final long producerCredit() { + lock(); + try { + return BUFFER_SIZE - available(); + } finally { + unlock(); + } + } + + /** + * {@return the credit of the consumer} + * @implSpec + * This is equivalent to {@link #maxStreamData()} - {@link #bytesConsumed()}. + */ + public final long consumerCredit() { + lock(); + try { + return maxStreamData - bytesConsumed; + } finally { + unlock(); + } + } + + /** + * {@return the amount of available data that can be sent + * with respect to flow control in this stream}. + * This does not take into account the global connection + * flow control. + */ + public final long readyToSend() { + long consumed, produced, max; + lock(); + try { + consumed = bytesConsumed; + max = maxStreamData; + produced = bytesProduced; + } finally { + unlock(); + } + assert max >= consumed; + assert produced >= consumed; + return Math.min(max - consumed, produced - consumed); + } + + public final void markReset() { + lock(); + try { + resetRequested = true; + } finally { + unlock(); + } + } + + final void close() { + lock(); + try { + bytesProduced = bytesConsumed; + queue.clear(); + current = null; + } finally { + unlock(); + } + } + + /** + * Called when a stop sending frame is received for this stream + * @param errorCode the error code + */ + protected final boolean stopSending(long errorCode) { + long stopSending; + lock(); + try { + if (resetRequested) return false; + if (streamSize >= 0 && bytesConsumed == streamSize) return false; + if ((stopSending = this.stopSending) < 0) return false; + this.stopSending = stopSending = - (errorCode + 1); + } finally { + unlock(); + } + assert stopSending < 0 && stopSending == - (errorCode + 1); + return true; + } + + /** + * {@return -1 minus the error code that was supplied by the peer + * when requesting for stop sending} + * @apiNote a strictly negative value indicates that the stream was + * reset by the peer. The error code supplied by the peer + * can be obtained with the formula:

    {@code
    +     *    long errorCode = - (resetByPeer() + 1);
    +     *    }
    + */ + final long resetByPeer() { + return stopSending; + } + + /** + * This method is called to wake up the consumer when there is + * credit and data available for the consumer. + */ + protected abstract void wakeupConsumer(); + + /** + * This method is called to wake up the producer when there is + * credit available for the producer. + */ + protected abstract void wakeupProducer(); + + /** + * Called to switch the sending state when data has been sent. + * @param dataSent the new state - typically {@link SendingStreamState#DATA_SENT} + */ + protected abstract void switchState(SendingStreamState dataSent); + + /** + * {@return the stream id this queue was created for} + */ + protected abstract long streamId(); + +} diff --git a/src/java.net.http/share/classes/module-info.java b/src/java.net.http/share/classes/module-info.java index 14cbb85291d..392385136b0 100644 --- a/src/java.net.http/share/classes/module-info.java +++ b/src/java.net.http/share/classes/module-info.java @@ -75,6 +75,18 @@ *
  • {@systemProperty jdk.httpclient.hpack.maxheadertablesize} (default: 16384 or * 16 kB)
    The HTTP/2 client maximum HPACK header table size in bytes. *

  • + *
  • {@systemProperty jdk.httpclient.qpack.decoderMaxTableCapacity} (default: 0) + *
    The HTTP/3 client maximum QPACK decoder dynamic header table size in bytes. + *
    Setting this value to a positive number will allow HTTP/3 servers to add entries + * to the QPack decoder's dynamic table. When set to 0, servers are not permitted to add + * entries to the client's QPack encoder's dynamic table. + *

  • + *
  • {@systemProperty jdk.httpclient.qpack.encoderTableCapacityLimit} (default: 4096, + * or 4 kB) + *
    The HTTP/3 client maximum QPACK encoder dynamic header table size in bytes. + *
    Setting this value to a positive number allows the HTTP/3 client's QPack encoder to + * add entries to the server's QPack decoder's dynamic table, if the server permits it. + *

  • *
  • {@systemProperty jdk.httpclient.HttpClient.log} (default: none)
    * Enables high-level logging of various events through the {@linkplain java.lang.System.Logger * Platform Logging API}. The value contains a comma-separated list of any of the @@ -88,6 +100,8 @@ *

  • ssl
  • *
  • trace
  • *
  • channel
  • + *
  • http3
  • + *
  • quic
  • *
    * You can append the frames item with a colon-separated list of any of the following items: *
      @@ -96,31 +110,57 @@ *
    • window
    • *
    • all
    • *

    + * You can append the quic item with a colon-separated list of any of the following items; + * packets are logged in an abridged form that only shows frames offset and length, + * but not content: + *
      + *
    • ack: packets containing ack frames will be logged
    • + *
    • cc: information on congestion control will be logged
    • + *
    • control: packets containing quic controls (such as frames affecting + * flow control, or frames opening or closing streams) + * will be logged
    • + *
    • crypto: packets containing crypto frames will be logged
    • + *
    • data: packets containing stream frames will be logged
    • + *
    • dbb: information on direct byte buffer usage will be logged
    • + *
    • ping: packets containing ping frames will be logged
    • + *
    • processed: information on flow control (processed bytes) will be logged
    • + *
    • retransmit: information on packet loss and recovery will be logged
    • + *
    • timer: information on send task scheduling will be logged
    • + *
    • all
    • + *

    * Specifying an item adds it to the HTTP client's log. For example, if you specify the * following value, then the Platform Logging API logs all possible HTTP Client events:
    * "errors,requests,headers,frames:control:data:window,ssl,trace,channel"
    * Note that you can replace control:data:window with all. The name of the logger is * "jdk.httpclient.HttpClient", and all logging is at level INFO. + * To debug issues with the quic protocol a good starting point is to specify + * {@code quic:control:retransmit}. * *
  • {@systemProperty jdk.httpclient.keepalive.timeout} (default: 30)
    - * The number of seconds to keep idle HTTP connections alive in the keep alive cache. This - * property applies to both HTTP/1.1 and HTTP/2. The value for HTTP/2 can be overridden - * with the {@code jdk.httpclient.keepalive.timeout.h2 property}. + * The number of seconds to keep idle HTTP connections alive in the keep alive cache. + * By default this property applies to HTTP/1.1, HTTP/2 and HTTP/3. + * The value for HTTP/2 and HTTP/3 can be overridden with the + * {@code jdk.httpclient.keepalive.timeout.h2} and {@code jdk.httpclient.keepalive.timeout.h3} + * properties respectively. The value specified for HTTP/2 acts as default value for HTTP/3. *

  • *
  • {@systemProperty jdk.httpclient.keepalive.timeout.h2} (default: see * below)
    The number of seconds to keep idle HTTP/2 connections alive. If not set, then the * {@code jdk.httpclient.keepalive.timeout} setting is used. *

  • + *
  • {@systemProperty jdk.httpclient.keepalive.timeout.h3} (default: see + * below)
    The number of seconds to keep idle HTTP/3 connections alive. If not set, then the + * {@code jdk.httpclient.keepalive.timeout.h2} setting is used. + *

  • *
  • {@systemProperty jdk.httpclient.maxframesize} (default: 16384 or 16kB)
    * The HTTP/2 client maximum frame size in bytes. The server is not permitted to send a frame * larger than this. *

  • *
  • {@systemProperty jdk.httpclient.maxLiteralWithIndexing} (default: 512)
    * The maximum number of header field lines (header name and value pairs) that a - * client is willing to add to the HPack Decoder dynamic table during the decoding + * client is willing to add to the HPack or QPACK Decoder dynamic table during the decoding * of an entire header field section. * This is purely an implementation limit. - * If a peer sends a field section with encoding that + * If a peer sends a field section or a set of QPACK instructions with encoding that * exceeds this limit a {@link java.net.ProtocolException ProtocolException} will be raised. * A value of zero or a negative value means no limit. *

  • @@ -135,7 +175,7 @@ * A value of zero or a negative value means no limit. * *
  • {@systemProperty jdk.httpclient.maxstreams} (default: 100)
    - * The maximum number of HTTP/2 push streams that the client will permit servers to open + * The maximum number of HTTP/2 or HTTP/3 push streams that the client will permit servers to open * simultaneously. *

  • *
  • {@systemProperty jdk.httpclient.receiveBufferSize} (default: operating system @@ -187,6 +227,61 @@ * value means no limit. *

  • * + *

    + * The following system properties can be used to configure some aspects of the + * QUIC Protocol + * implementation used for HTTP/3: + *

      + *
    • {@systemProperty jdk.httpclient.quic.receiveBufferSize} (default: operating system + * default)
      The QUIC {@linkplain java.nio.channels.DatagramChannel UDP client socket} + * {@linkplain java.net.StandardSocketOptions#SO_RCVBUF receive buffer size} in bytes. + * Values less than or equal to zero are ignored. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.sendBufferSize} (default: operating system + * default)
      The QUIC {@linkplain java.nio.channels.DatagramChannel UDP client socket} + * {@linkplain java.net.StandardSocketOptions#SO_SNDBUF send buffer size} in bytes. + * Values less than or equal to zero are ignored. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.defaultMTU} (default: 1200 bytes)
      + * The default Maximum Transmission Unit (MTU) size that will be used on quic connections. + * The default implementation of the HTTP/3 client does not implement Path MTU Detection, + * but will attempt to send 1-RTT packets up to the size defined by this property. + * Specifying a higher value may give better upload performance when the client and + * servers are located on the same machine, but is likely to result in irrecoverable + * packet loss if used over the network. Allowed values are in the range [1200, 65527]. + * If an out-of-range value is specified, the minimum default value will be used. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxBytesInFlight} (default: + * 16777216 bytes or 16MB)
      + * This is the maximum number of unacknowledged bytes that the quic congestion + * controller allows to be in flight. When this amount is reached, no new + * data is sent until some of the packets in flight are acknowledged. + *
      + * Allowed values are in the range [2^14, 2^24] (or [16kB, 16MB]). + * If an out-of-range value is specified, it will be clamped to the closest + * value in range. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxInitialData} (default: 15728640 + * bytes, or 15MB)
      + * The initial flow control limit for quic connections in bytes. Valid values are in + * the range [0, 2^60]. The initial limit is also used to initialize the receive window + * size. If less than 16kB, the window size will be set to 16kB. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxStreamInitialData} (default: 6291456 + * bytes, or 6MB)
      + * The initial flow control limit for quic streams in bytes. Valid values are in + * the range [0, 2^60]. The initial limit is also used to initialize the receive window + * size. If less than 16kB, the window size will be set to 16kB. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxInitialTimeout} (default: 30 + * seconds)
      + * This is the maximum time, in seconds, during which the client will wait for a + * response from the server, and continue retransmitting the first Quic INITIAL packet, + * before raising a {@link java.net.ConnectException}. The first INITIAL packet received + * from the target server will disarm this timeout. + *

    • + * + *
    * @moduleGraph * @since 11 */ diff --git a/src/java.scripting/share/classes/com/sun/tools/script/shell/Main.java b/src/java.scripting/share/classes/com/sun/tools/script/shell/Main.java deleted file mode 100644 index 96fd6d010ac..00000000000 --- a/src/java.scripting/share/classes/com/sun/tools/script/shell/Main.java +++ /dev/null @@ -1,595 +0,0 @@ -/* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.sun.tools.script.shell; - -import java.io.*; -import java.net.*; -import java.nio.charset.Charset; -import java.text.*; -import java.util.*; -import javax.script.*; - -/** - * This is the main class for Java script shell. - */ -public class Main { - /** - * main entry point to the command line tool - * @param args command line argument array - */ - public static void main(String[] args) { - // print deprecation warning - getError().println(getMessage("deprecated.warning", - new Object[] { PROGRAM_NAME })); - - // parse command line options - String[] scriptArgs = processOptions(args); - - // process each script command - for (Command cmd : scripts) { - cmd.run(scriptArgs); - } - - System.exit(EXIT_SUCCESS); - } - - // Each -e or -f or interactive mode is represented - // by an instance of Command. - private static interface Command { - public void run(String[] arguments); - } - - /** - * Parses and processes command line options. - * @param args command line argument array - */ - private static String[] processOptions(String[] args) { - // current scripting language selected - String currentLanguage = DEFAULT_LANGUAGE; - // current script file encoding selected - String currentEncoding = null; - - // check for -classpath or -cp first - checkClassPath(args); - - // have we seen -e or -f ? - boolean seenScript = false; - // have we seen -f - already? - boolean seenStdin = false; - for (int i=0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("-classpath") || - arg.equals("-cp")) { - // handled already, just continue - i++; - continue; - } - - // collect non-option arguments and pass these as script arguments - if (!arg.startsWith("-")) { - int numScriptArgs; - int startScriptArg; - if (seenScript) { - // if we have seen -e or -f already all non-option arguments - // are passed as script arguments - numScriptArgs = args.length - i; - startScriptArg = i; - } else { - // if we have not seen -e or -f, first non-option argument - // is treated as script file name and rest of the non-option - // arguments are passed to script as script arguments - numScriptArgs = args.length - i - 1; - startScriptArg = i + 1; - ScriptEngine se = getScriptEngine(currentLanguage); - addFileSource(se, args[i], currentEncoding); - } - // collect script arguments and return to main - String[] result = new String[numScriptArgs]; - System.arraycopy(args, startScriptArg, result, 0, numScriptArgs); - return result; - } - - if (arg.startsWith("-D")) { - String value = arg.substring(2); - int eq = value.indexOf('='); - if (eq != -1) { - System.setProperty(value.substring(0, eq), - value.substring(eq + 1)); - } else { - if (!value.isEmpty()) { - System.setProperty(value, ""); - } else { - // do not allow empty property name - usage(EXIT_CMD_NO_PROPNAME); - } - } - continue; - } else if (arg.equals("-?") || - arg.equals("-h") || - arg.equals("--help") || - // -help: legacy. - arg.equals("-help")) { - usage(EXIT_SUCCESS); - } else if (arg.equals("-e")) { - seenScript = true; - if (++i == args.length) - usage(EXIT_CMD_NO_SCRIPT); - - ScriptEngine se = getScriptEngine(currentLanguage); - addStringSource(se, args[i]); - continue; - } else if (arg.equals("-encoding")) { - if (++i == args.length) - usage(EXIT_CMD_NO_ENCODING); - currentEncoding = args[i]; - continue; - } else if (arg.equals("-f")) { - seenScript = true; - if (++i == args.length) - usage(EXIT_CMD_NO_FILE); - ScriptEngine se = getScriptEngine(currentLanguage); - if (args[i].equals("-")) { - if (seenStdin) { - usage(EXIT_MULTIPLE_STDIN); - } else { - seenStdin = true; - } - addInteractiveMode(se); - } else { - addFileSource(se, args[i], currentEncoding); - } - continue; - } else if (arg.equals("-l")) { - if (++i == args.length) - usage(EXIT_CMD_NO_LANG); - currentLanguage = args[i]; - continue; - } else if (arg.equals("-q")) { - listScriptEngines(); - } - // some unknown option... - usage(EXIT_UNKNOWN_OPTION); - } - - if (! seenScript) { - ScriptEngine se = getScriptEngine(currentLanguage); - addInteractiveMode(se); - } - return new String[0]; - } - - /** - * Adds interactive mode Command - * @param se ScriptEngine to use in interactive mode. - */ - private static void addInteractiveMode(final ScriptEngine se) { - scripts.add(new Command() { - public void run(String[] args) { - setScriptArguments(se, args); - processSource(se, "-", null); - } - }); - } - - /** - * Adds script source file Command - * @param se ScriptEngine used to evaluate the script file - * @param fileName script file name - * @param encoding script file encoding - */ - private static void addFileSource(final ScriptEngine se, - final String fileName, - final String encoding) { - scripts.add(new Command() { - public void run(String[] args) { - setScriptArguments(se, args); - processSource(se, fileName, encoding); - } - }); - } - - /** - * Adds script string source Command - * @param se ScriptEngine to be used to evaluate the script string - * @param source Script source string - */ - private static void addStringSource(final ScriptEngine se, - final String source) { - scripts.add(new Command() { - public void run(String[] args) { - setScriptArguments(se, args); - String oldFile = setScriptFilename(se, ""); - try { - evaluateString(se, source); - } finally { - setScriptFilename(se, oldFile); - } - } - }); - } - - /** - * Prints list of script engines available and exits. - */ - private static void listScriptEngines() { - List factories = engineManager.getEngineFactories(); - for (ScriptEngineFactory factory: factories) { - getError().println(getMessage("engine.info", - new Object[] { factory.getLanguageName(), - factory.getLanguageVersion(), - factory.getEngineName(), - factory.getEngineVersion() - })); - } - System.exit(EXIT_SUCCESS); - } - - /** - * Processes a given source file or standard input. - * @param se ScriptEngine to be used to evaluate - * @param filename file name, can be null - * @param encoding script file encoding, can be null - */ - private static void processSource(ScriptEngine se, String filename, - String encoding) { - if (filename.equals("-")) { - Charset charset = Charset.forName(System.getProperty("stdin.encoding"), Charset.defaultCharset()); - BufferedReader in = new BufferedReader(new InputStreamReader(System.in, charset)); - boolean hitEOF = false; - String prompt = getPrompt(se); - se.put(ScriptEngine.FILENAME, ""); - while (!hitEOF) { - getError().print(prompt); - String source = ""; - try { - source = in.readLine(); - } catch (IOException ioe) { - getError().println(ioe.toString()); - } - if (source == null) { - hitEOF = true; - break; - } - Object res = evaluateString(se, source, false); - if (res != null) { - res = res.toString(); - if (res == null) { - res = "null"; - } - getError().println(res); - } - } - } else { - FileInputStream fis = null; - try { - fis = new FileInputStream(filename); - } catch (FileNotFoundException fnfe) { - getError().println(getMessage("file.not.found", - new Object[] { filename })); - System.exit(EXIT_FILE_NOT_FOUND); - } - evaluateStream(se, fis, filename, encoding); - } - } - - /** - * Evaluates given script source - * @param se ScriptEngine to evaluate the string - * @param script Script source string - * @param exitOnError whether to exit the process on script error - */ - private static Object evaluateString(ScriptEngine se, - String script, boolean exitOnError) { - try { - return se.eval(script); - } catch (ScriptException sexp) { - getError().println(getMessage("string.script.error", - new Object[] { sexp.getMessage() })); - if (exitOnError) - System.exit(EXIT_SCRIPT_ERROR); - } catch (Exception exp) { - exp.printStackTrace(getError()); - if (exitOnError) - System.exit(EXIT_SCRIPT_ERROR); - } - - return null; - } - - /** - * Evaluate script string source and exit on script error - * @param se ScriptEngine to evaluate the string - * @param script Script source string - */ - private static void evaluateString(ScriptEngine se, String script) { - evaluateString(se, script, true); - } - - /** - * Evaluates script from given reader - * @param se ScriptEngine to evaluate the string - * @param reader Reader from which is script is read - * @param name file name to report in error. - */ - private static Object evaluateReader(ScriptEngine se, - Reader reader, String name) { - String oldFilename = setScriptFilename(se, name); - try { - return se.eval(reader); - } catch (ScriptException sexp) { - getError().println(getMessage("file.script.error", - new Object[] { name, sexp.getMessage() })); - System.exit(EXIT_SCRIPT_ERROR); - } catch (Exception exp) { - exp.printStackTrace(getError()); - System.exit(EXIT_SCRIPT_ERROR); - } finally { - setScriptFilename(se, oldFilename); - } - return null; - } - - /** - * Evaluates given input stream - * @param se ScriptEngine to evaluate the string - * @param is InputStream from which script is read - * @param name file name to report in error - */ - private static Object evaluateStream(ScriptEngine se, - InputStream is, String name, - String encoding) { - BufferedReader reader = null; - if (encoding != null) { - try { - reader = new BufferedReader(new InputStreamReader(is, - encoding)); - } catch (UnsupportedEncodingException uee) { - getError().println(getMessage("encoding.unsupported", - new Object[] { encoding })); - System.exit(EXIT_NO_ENCODING_FOUND); - } - } else { - reader = new BufferedReader(new InputStreamReader(is)); - } - return evaluateReader(se, reader, name); - } - - /** - * Prints usage message and exits - * @param exitCode process exit code - */ - private static void usage(int exitCode) { - getError().println(getMessage("main.usage", - new Object[] { PROGRAM_NAME })); - System.exit(exitCode); - } - - /** - * Gets prompt for interactive mode - * @return prompt string to use - */ - private static String getPrompt(ScriptEngine se) { - List names = se.getFactory().getNames(); - return names.get(0) + "> "; - } - - /** - * Get formatted, localized error message - */ - private static String getMessage(String key, Object[] params) { - return MessageFormat.format(msgRes.getString(key), params); - } - - // stream to print error messages - private static PrintStream getError() { - return System.err; - } - - // get current script engine - private static ScriptEngine getScriptEngine(String lang) { - ScriptEngine se = engines.get(lang); - if (se == null) { - se = engineManager.getEngineByName(lang); - if (se == null) { - getError().println(getMessage("engine.not.found", - new Object[] { lang })); - System.exit(EXIT_ENGINE_NOT_FOUND); - } - - // initialize the engine - initScriptEngine(se); - // to avoid re-initialization of engine, store it in a map - engines.put(lang, se); - } - return se; - } - - // initialize a given script engine - private static void initScriptEngine(ScriptEngine se) { - // put engine global variable - se.put("engine", se); - - // load init. file from resource - List exts = se.getFactory().getExtensions(); - InputStream sysIn = null; - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - for (String ext : exts) { - try { - sysIn = Main.class.getModule().getResourceAsStream("com/sun/tools/script/shell/init." + ext); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - if (sysIn != null) break; - } - if (sysIn != null) { - evaluateStream(se, sysIn, "", null); - } - } - - /** - * Checks for -classpath, -cp in command line args. Creates a ClassLoader - * and sets it as Thread context loader for current thread. - * - * @param args command line argument array - */ - private static void checkClassPath(String[] args) { - String classPath = null; - for (int i = 0; i < args.length; i++) { - if (args[i].equals("-classpath") || - args[i].equals("-cp")) { - if (++i == args.length) { - // just -classpath or -cp with no value - usage(EXIT_CMD_NO_CLASSPATH); - } else { - classPath = args[i]; - } - } - } - - if (classPath != null) { - /* We create a class loader, configure it with specified - * classpath values and set the same as context loader. - * Note that ScriptEngineManager uses context loader to - * load script engines. So, this ensures that user defined - * script engines will be loaded. For classes referred - * from scripts, Rhino engine uses thread context loader - * but this is script engine dependent. We don't have - * script engine independent solution anyway. Unless we - * know the class loader used by a specific engine, we - * can't configure correct loader. - */ - URL[] urls = pathToURLs(classPath); - URLClassLoader loader = new URLClassLoader(urls); - Thread.currentThread().setContextClassLoader(loader); - } - - // now initialize script engine manager. Note that this has to - // be done after setting the context loader so that manager - // will see script engines from user specified classpath - engineManager = new ScriptEngineManager(); - } - - /** - * Utility method for converting a search path string to an array - * of directory and JAR file URLs. - * - * @param path the search path string - * @return the resulting array of directory and JAR file URLs - */ - private static URL[] pathToURLs(String path) { - String[] components = path.split(File.pathSeparator); - URL[] urls = new URL[components.length]; - int count = 0; - while(count < components.length) { - URL url = fileToURL(new File(components[count])); - if (url != null) { - urls[count++] = url; - } - } - if (urls.length != count) { - URL[] tmp = new URL[count]; - System.arraycopy(urls, 0, tmp, 0, count); - urls = tmp; - } - return urls; - } - - /** - * Returns the directory or JAR file URL corresponding to the specified - * local file name. - * - * @param file the File object - * @return the resulting directory or JAR file URL, or null if unknown - */ - private static URL fileToURL(File file) { - String name; - try { - name = file.getCanonicalPath(); - } catch (IOException e) { - name = file.getAbsolutePath(); - } - name = name.replace(File.separatorChar, '/'); - if (!name.startsWith("/")) { - name = "/" + name; - } - // If the file does not exist, then assume that it's a directory - if (!file.isFile()) { - name = name + "/"; - } - try { - @SuppressWarnings("deprecation") - var result = new URL("file", "", name); - return result; - } catch (MalformedURLException e) { - throw new IllegalArgumentException("file"); - } - } - - private static void setScriptArguments(ScriptEngine se, String[] args) { - se.put("arguments", args); - se.put(ScriptEngine.ARGV, args); - } - - private static String setScriptFilename(ScriptEngine se, String name) { - String oldName = (String) se.get(ScriptEngine.FILENAME); - se.put(ScriptEngine.FILENAME, name); - return oldName; - } - - // exit codes - private static final int EXIT_SUCCESS = 0; - private static final int EXIT_CMD_NO_CLASSPATH = 1; - private static final int EXIT_CMD_NO_FILE = 2; - private static final int EXIT_CMD_NO_SCRIPT = 3; - private static final int EXIT_CMD_NO_LANG = 4; - private static final int EXIT_CMD_NO_ENCODING = 5; - private static final int EXIT_CMD_NO_PROPNAME = 6; - private static final int EXIT_UNKNOWN_OPTION = 7; - private static final int EXIT_ENGINE_NOT_FOUND = 8; - private static final int EXIT_NO_ENCODING_FOUND = 9; - private static final int EXIT_SCRIPT_ERROR = 10; - private static final int EXIT_FILE_NOT_FOUND = 11; - private static final int EXIT_MULTIPLE_STDIN = 12; - - // default scripting language - private static final String DEFAULT_LANGUAGE = "js"; - // list of scripts to process - private static List scripts; - // the script engine manager - private static ScriptEngineManager engineManager; - // map of engines we loaded - private static Map engines; - // error messages resource - private static ResourceBundle msgRes; - private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages"; - private static String PROGRAM_NAME = "jrunscript"; - - static { - scripts = new ArrayList(); - engines = new HashMap(); - msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault()); - } -} diff --git a/src/java.scripting/share/classes/com/sun/tools/script/shell/init.js b/src/java.scripting/share/classes/com/sun/tools/script/shell/init.js deleted file mode 100644 index ced3ba06367..00000000000 --- a/src/java.scripting/share/classes/com/sun/tools/script/shell/init.js +++ /dev/null @@ -1,927 +0,0 @@ -/* - * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/** - * jrunscript JavaScript built-in functions and objects. - */ - -/** - * Creates an object that delegates all method calls on - * it to the 'invoke' method on the given delegate object.
    - * - * Example: - *
    - * 
    - *     var x  = { invoke: function(name, args) { //code...}
    - *     var y = new JSInvoker(x);
    - *     y.func(3, 3); // calls x.invoke('func', args); where args is array of arguments
    - * 
    - * 
    - * @param obj object to be wrapped by JSInvoker - * @constructor - */ -function JSInvoker(obj) { - return new JSAdapter({ - __get__ : function(name) { - return function() { - return obj.invoke(name, arguments); - } - } - }); -} - -/** - * This variable represents OS environment. Environment - * variables can be accessed as fields of this object. For - * example, env.PATH will return PATH value configured. - */ -var env = new JSAdapter({ - __get__ : function (name) { - return java.lang.System.getenv(name); - }, - __has__ : function (name) { - return java.lang.System.getenv().containsKey(name); - }, - __getIds__ : function() { - return java.lang.System.getenv().keySet().toArray(); - }, - __delete__ : function(name) { - println("can't delete env item"); - }, - __put__ : function (name, value) { - println("can't change env item"); - }, - toString: function() { - return java.lang.System.getenv().toString(); - } -}); - -/** - * Creates a convenient script object to deal with java.util.Map instances. - * The result script object's field names are keys of the Map. For example, - * scriptObj.keyName can be used to access value associated with given key.
    - * Example: - *
    - * 
    - *     var x = java.lang.SystemProperties();
    - *     var y = jmap(x);
    - *     println(y['java.class.path']); // prints java.class.path System property
    - *     delete y['java.class.path']; // remove java.class.path System property
    - * 
    - * 
    - * - * @param map java.util.Map instance that will be wrapped - * @constructor - */ -function jmap(map) { - return new JSAdapter({ - __get__ : function(name) { - if (map.containsKey(name)) { - return map.get(name); - } else { - return undefined; - } - }, - __has__ : function(name) { - return map.containsKey(name); - }, - - __delete__ : function (name) { - return map.remove(name); - }, - __put__ : function(name, value) { - map.put(name, value); - }, - __getIds__ : function() { - return map.keySet().toArray(); - }, - toString: function() { - return map.toString(); - } - }); -} - -/** - * Creates a convenient script object to deal with java.util.List instances. - * The result script object behaves like an array. For example, - * scriptObj[index] syntax can be used to access values in the List instance. - * 'length' field gives size of the List.
    - * - * Example: - *
    - * 
    - *    var x = new java.util.ArrayList(4);
    - *    x.add('Java');
    - *    x.add('JavaScript');
    - *    x.add('SQL');
    - *    x.add('XML');
    - *
    - *    var y = jlist(x);
    - *    println(y[2]); // prints third element of list
    - *    println(y.length); // prints size of the list
    - *
    - * @param map java.util.List instance that will be wrapped
    - * @constructor
    - */
    -function jlist(list) {
    -    function isValid(index) {
    -        return typeof(index) == 'number' &&
    -            index > -1 && index < list.size();
    -    }
    -    return new JSAdapter({
    -        __get__ :  function(name) {
    -            if (isValid(name)) {
    -                return list.get(name);
    -            } else if (name == 'length') {
    -                return list.size();
    -            } else {
    -                return undefined;
    -            }
    -        },
    -        __has__ : function (name) {
    -            return isValid(name) || name == 'length';
    -        },
    -        __delete__ : function(name) {
    -            if (isValid(name)) {
    -                list.remove(name);
    -            }
    -        },
    -        __put__ : function(name, value) {
    -            if (isValid(name)) {
    -                list.set(name, value);
    -            }
    -        },
    -        __getIds__: function() {
    -            var res = new Array(list.size());
    -            for (var i = 0; i < res.length; i++) {
    -                res[i] = i;
    -            }
    -            return res;
    -        },
    -        toString: function() {
    -            return list.toString();
    -        }
    -    });
    -}
    -
    -/**
    - * This is java.lang.System properties wrapped by JSAdapter.
    - * For eg. to access java.class.path property, you can use
    - * the syntax sysProps["java.class.path"]
    - */
    -var sysProps = new JSAdapter({
    -    __get__ : function (name) {
    -        return java.lang.System.getProperty(name);
    -    },
    -    __has__ : function (name) {
    -        return java.lang.System.getProperty(name) != null;
    -    },
    -    __getIds__ : function() {
    -        return java.lang.System.getProperties().keySet().toArray();
    -    },
    -    __delete__ : function(name) {
    -        java.lang.System.clearProperty(name);
    -        return true;
    -    },
    -    __put__ : function (name, value) {
    -        java.lang.System.setProperty(name, value);
    -    },
    -    toString: function() {
    -        return "";
    -    }
    -});
    -
    -// stdout, stderr & stdin
    -var out = java.lang.System.out;
    -var err = java.lang.System.err;
    -// can't use 'in' because it is a JavaScript keyword :-(
    -var inp = java.lang.System["in"];
    -
    -var BufferedInputStream = java.io.BufferedInputStream;
    -var BufferedOutputStream = java.io.BufferedOutputStream;
    -var BufferedReader = java.io.BufferedReader;
    -var DataInputStream = java.io.DataInputStream;
    -var File = java.io.File;
    -var FileInputStream = java.io.FileInputStream;
    -var FileOutputStream = java.io.FileOutputStream;
    -var InputStream = java.io.InputStream;
    -var InputStreamReader = java.io.InputStreamReader;
    -var OutputStream = java.io.OutputStream;
    -var Reader = java.io.Reader;
    -var URL = java.net.URL;
    -
    -/**
    - * Generic any object to input stream mapper
    - * @param str input file name, URL or InputStream
    - * @return InputStream object
    - * @private
    - */
    -function inStream(str) {
    -    if (typeof(str) == "string") {
    -        // '-' means standard input
    -        if (str == '-') {
    -            return java.lang.System["in"];
    -        }
    -        // try file first
    -        var file = null;
    -        try {
    -            file = pathToFile(str);
    -        } catch (e) {
    -        }
    -        if (file && file.exists()) {
    -            return new FileInputStream(file);
    -        } else {
    -            try {
    -                // treat the string as URL
    -                return new URL(str).openStream();
    -            } catch (e) {
    -                throw 'file or URL ' + str + ' not found';
    -            }
    -        }
    -    } else {
    -        if (str instanceof InputStream) {
    -            return str;
    -        } else if (str instanceof URL) {
    -            return str.openStream();
    -        } else if (str instanceof File) {
    -            return new FileInputStream(str);
    -        }
    -    }
    -    // everything failed, just give input stream
    -    return java.lang.System["in"];
    -}
    -
    -/**
    - * Generic any object to output stream mapper
    - *
    - * @param out output file name or stream
    - * @return OutputStream object
    - * @private
    - */
    -function outStream(out) {
    -    if (typeof(out) == "string") {
    -        if (out == '>') {
    -            return java.lang.System.out;
    -        } else {
    -            // treat it as file
    -            return new FileOutputStream(pathToFile(out));
    -        }
    -    } else {
    -        if (out instanceof OutputStream) {
    -            return out;
    -        } else if (out instanceof File) {
    -            return new FileOutputStream(out);
    -        }
    -    }
    -
    -    // everything failed, just return System.out
    -    return java.lang.System.out;
    -}
    -
    -/**
    - * stream close takes care not to close stdin, out & err.
    - * @private
    - */
    -function streamClose(stream) {
    -    if (stream) {
    -        if (stream != java.lang.System["in"] &&
    -            stream != java.lang.System.out &&
    -            stream != java.lang.System.err) {
    -            try {
    -                stream.close();
    -            } catch (e) {
    -                println(e);
    -            }
    -        }
    -    }
    -}
    -
    -/**
    - * Loads and evaluates JavaScript code from a stream or file or URL
    - * - * Examples: - *
    - * 
    - *    load('test.js'); // load script file 'test.js'
    - *    load('http://java.sun.com/foo.js'); // load from a URL
    - * 
    - * 
    - * - * @param str input from which script is loaded and evaluated - */ -if (typeof(load) == 'undefined') { - this.load = function(str) { - var stream = inStream(str); - var bstream = new BufferedInputStream(stream); - var reader = new BufferedReader(new InputStreamReader(bstream)); - var oldFilename = engine.get(engine.FILENAME); - engine.put(engine.FILENAME, str); - try { - engine.eval(reader); - } finally { - engine.put(engine.FILENAME, oldFilename); - streamClose(stream); - } - } -} - -// file system utilities - -/** - * Creates a Java byte[] of given length - * @param len size of the array to create - * @private - */ -function javaByteArray(len) { - return java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, len); -} - -var curDir = new File('.'); - -/** - * Print present working directory - */ -function pwd() { - println(curDir.getAbsolutePath()); -} - -/** - * Changes present working directory to given directory - * @param target directory to change to. optional, defaults to user's HOME - */ -function cd(target) { - if (target == undefined) { - target = sysProps["user.home"]; - } - if (!(target instanceof File)) { - target = pathToFile(target); - } - if (target.exists() && target.isDirectory()) { - curDir = target; - } else { - println(target + " is not a directory"); - } -} - -/** - * Converts path to java.io.File taking care of shell present working dir - * - * @param pathname file path to be converted - * @private - */ -function pathToFile(pathname) { - var tmp = pathname; - if (!(tmp instanceof File)) { - tmp = new File(tmp); - } - if (!tmp.isAbsolute()) { - return new File(curDir, pathname); - } else { - return tmp; - } -} - -/** - * Copies a file or URL or stream to another file or stream - * - * @param from input file or URL or stream - * @param to output stream or file - */ -function cp(from, to) { - if (from == to) { - println("file " + from + " cannot be copied onto itself!"); - return; - } - var inp = inStream(from); - var out = outStream(to); - var binp = new BufferedInputStream(inp); - var bout = new BufferedOutputStream(out); - var buff = javaByteArray(1024); - var len; - while ((len = binp.read(buff)) > 0 ) - bout.write(buff, 0, len); - - bout.flush(); - streamClose(inp); - streamClose(out); -} - -/** - * Shows the content of a file or URL or any InputStream
    - * Examples: - *
    - * 
    - *    cat('test.txt'); // show test.txt file contents
    - *    cat('http://java.net'); // show the contents from the URL http://java.net
    - * 
    - * 
    - * @param obj input to show - * @param pattern optional. show only the lines matching the pattern - */ -function cat(obj, pattern) { - if (obj instanceof File && obj.isDirectory()) { - ls(obj); - return; - } - - var inp = null; - if (!(obj instanceof Reader)) { - inp = inStream(obj); - obj = new BufferedReader(new InputStreamReader(inp)); - } - var line; - if (pattern) { - var count = 1; - while ((line=obj.readLine()) != null) { - if (line.match(pattern)) { - println(count + "\t: " + line); - } - count++; - } - } else { - while ((line=obj.readLine()) != null) { - println(line); - } - } -} - -/** - * Returns directory part of a filename - * - * @param pathname input path name - * @return directory part of the given file name - */ -function dirname(pathname) { - var dirName = "."; - // Normalize '/' to local file separator before work. - var i = pathname.replace('/', File.separatorChar ).lastIndexOf( - File.separator ); - if ( i != -1 ) - dirName = pathname.substring(0, i); - return dirName; -} - -/** - * Creates a new dir of given name - * - * @param dir name of the new directory - */ -function mkdir(dir) { - dir = pathToFile(dir); - println(dir.mkdir()? "created" : "can not create dir"); -} - -/** - * Creates the directory named by given pathname, including - * any necessary but nonexistent parent directories. - * - * @param dir input path name - */ -function mkdirs(dir) { - dir = pathToFile(dir); - println(dir.mkdirs()? "created" : "can not create dirs"); -} - -/** - * Removes a given file - * - * @param pathname name of the file - */ -function rm(pathname) { - var file = pathToFile(pathname); - if (!file.exists()) { - println("file not found: " + pathname); - return false; - } - // note that delete is a keyword in JavaScript! - println(file["delete"]()? "deleted" : "can not delete"); -} - -/** - * Removes a given directory - * - * @param pathname name of the directory - */ -function rmdir(pathname) { - rm(pathname); -} - -/** - * Synonym for 'rm' - */ -function del(pathname) { - rm(pathname); -} - -/** - * Moves a file to another - * - * @param from original name of the file - * @param to new name for the file - */ -function mv(from, to) { - println(pathToFile(from).renameTo(pathToFile(to))? - "moved" : "can not move"); -} - -/** - * Synonym for 'mv'. - */ -function ren(from, to) { - mv(from, to); -} - -var months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; - -/** - * Helper function called by ls - * @private - */ -function printFile(f) { - var sb = new java.lang.StringBuffer(); - sb.append(f.isDirectory()? "d" : "-"); - sb.append(f.canRead() ? "r": "-" ); - sb.append(f.canWrite() ? "w": "-" ); - sb.append(" "); - - var d = new java.util.Date(f.lastModified()); - var c = new java.util.GregorianCalendar(); - c.setTime(d); - var day = c.get(java.util.Calendar.DAY_OF_MONTH); - sb.append(months[c.get(java.util.Calendar.MONTH)] - + " " + day ); - if (day < 10) { - sb.append(" "); - } - - // to get fixed length 'length' field - var fieldlen = 8; - var len = new java.lang.StringBuffer(); - for(var j=0; j - * - * Examples: - *
    - * 
    - *    find('.')
    - *    find('.', '.*\.class', rm);  // remove all .class files
    - *    find('.', '.*\.java');       // print fullpath of each .java file
    - *    find('.', '.*\.java', cat);  // print all .java files
    - * 
    - * 
    - * - * @param dir directory to search files - * @param pattern to search in the files - * @param callback function to call for matching files - */ -function find(dir, pattern, callback) { - dir = pathToFile(dir); - if (!callback) callback = print; - var files = dir.listFiles(); - for (var f in files) { - var file = files[f]; - if (file.isDirectory()) { - find(file, pattern, callback); - } else { - if (pattern) { - if (file.getName().match(pattern)) { - callback(file); - } - } else { - callback(file); - } - } - } -} - -// process utilities - -/** - * Exec's a child process, waits for completion & returns exit code - * - * @param cmd command to execute in child process - */ -function exec(cmd) { - var process = java.lang.Runtime.getRuntime().exec(cmd); - var inp = new DataInputStream(process.getInputStream()); - var line = null; - while ((line = inp.readLine()) != null) { - println(line); - } - process.waitFor(); - $exit = process.exitValue(); -} - -if (typeof(exit) == 'undefined') { - /** - * Exit the shell program. - * - * @param exitCode integer code returned to OS shell. - * optional, defaults to 0 - */ - this.exit = function (code) { - if (code) { - java.lang.System.exit(code + 0); - } else { - java.lang.System.exit(0); - } - } -} - -if (typeof(quit) == 'undefined') { - /** - * synonym for exit - */ - this.quit = function (code) { - exit(code); - } -} - -// XML utilities - -/** - * Converts input to DOM Document object - * - * @param inp file or reader. optional, without this param, - * this function returns a new DOM Document. - * @return returns a DOM Document object - */ -function XMLDocument(inp) { - var factory = javax.xml.parsers.DocumentBuilderFactory.newInstance(); - var builder = factory.newDocumentBuilder(); - if (inp) { - if (typeof(inp) == "string") { - return builder.parse(pathToFile(inp)); - } else { - return builder.parse(inp); - } - } else { - return builder.newDocument(); - } -} - -/** - * Converts arbitrary stream, file, URL to XMLSource - * - * @param inp input stream or file or URL - * @return XMLSource object - */ -function XMLSource(inp) { - if (inp instanceof javax.xml.transform.Source) { - return inp; - } else if (inp instanceof Packages.org.w3c.dom.Document) { - return new javax.xml.transform.dom.DOMSource(inp); - } else { - inp = new BufferedInputStream(inStream(inp)); - return new javax.xml.transform.stream.StreamSource(inp); - } -} - -/** - * Converts arbitrary stream, file to XMLResult - * - * @param inp output stream or file - * @return XMLResult object - */ -function XMLResult(out) { - if (out instanceof javax.xml.transform.Result) { - return out; - } else if (out instanceof Packages.org.w3c.dom.Document) { - return new javax.xml.transform.dom.DOMResult(out); - } else { - out = new BufferedOutputStream(outStream(out)); - return new javax.xml.transform.stream.StreamResult(out); - } -} - -/** - * Perform XSLT transform - * - * @param inp Input XML to transform (URL, File or InputStream) - * @param style XSL Stylesheet to be used (URL, File or InputStream). optional. - * @param out Output XML (File or OutputStream - */ -function XSLTransform(inp, style, out) { - switch (arguments.length) { - case 2: - inp = arguments[0]; - out = arguments[1]; - break; - case 3: - inp = arguments[0]; - style = arguments[1]; - out = arguments[2]; - break; - default: - println("XSL transform requires 2 or 3 arguments"); - return; - } - - var factory = javax.xml.transform.TransformerFactory.newInstance(); - var transformer; - if (style) { - transformer = factory.newTransformer(XMLSource(style)); - } else { - transformer = factory.newTransformer(); - } - var source = XMLSource(inp); - var result = XMLResult(out); - transformer.transform(source, result); - if (source.getInputStream) { - streamClose(source.getInputStream()); - } - if (result.getOutputStream) { - streamClose(result.getOutputStream()); - } -} - -// miscellaneous utilities - -/** - * Prints which command is selected from PATH - * - * @param cmd name of the command searched from PATH - */ -function which(cmd) { - var st = new java.util.StringTokenizer(env.PATH, File.pathSeparator); - while (st.hasMoreTokens()) { - var file = new File(st.nextToken(), cmd); - if (file.exists()) { - println(file.getAbsolutePath()); - return; - } - } -} - -/** - * Prints IP addresses of given domain name - * - * @param name domain name - */ -function ip(name) { - var addrs = InetAddress.getAllByName(name); - for (var i in addrs) { - println(addrs[i]); - } -} - -/** - * Prints current date in current locale - */ -function date() { - println(new Date().toLocaleString()); -} - -/** - * Echoes the given string arguments - */ -function echo(x) { - for (var i = 0; i < arguments.length; i++) { - println(arguments[i]); - } -} - -if (typeof(printf) == 'undefined') { - /** - * This is C-like printf - * - * @param format string to format the rest of the print items - * @param args variadic argument list - */ - this.printf = function (format, args/*, more args*/) { - var array = java.lang.reflect.Array.newInstance(java.lang.Object, - arguments.length - 1); - for (var i = 0; i < array.length; i++) { - array[i] = arguments[i+1]; - } - java.lang.System.out.printf(format, array); - } -} - -/** - * Reads one or more lines from stdin after printing prompt - * - * @param prompt optional, default is '>' - * @param multiline to tell whether to read single line or multiple lines - */ -function read(prompt, multiline) { - if (!prompt) { - prompt = '>'; - } - var inp = java.lang.System["in"]; - var reader = new BufferedReader(new InputStreamReader(inp)); - if (multiline) { - var line = ''; - while (true) { - java.lang.System.err.print(prompt); - java.lang.System.err.flush(); - var tmp = reader.readLine(); - if (tmp == '' || tmp == null) break; - line += tmp + '\n'; - } - return line; - } else { - java.lang.System.err.print(prompt); - java.lang.System.err.flush(); - return reader.readLine(); - } -} - -if (typeof(println) == 'undefined') { - // just synonym to print - this.println = print; -} - diff --git a/src/java.scripting/share/classes/com/sun/tools/script/shell/messages.properties b/src/java.scripting/share/classes/com/sun/tools/script/shell/messages.properties deleted file mode 100644 index a6c7762ed34..00000000000 --- a/src/java.scripting/share/classes/com/sun/tools/script/shell/messages.properties +++ /dev/null @@ -1,68 +0,0 @@ -# -# Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -string.script.error=\ - script error: {0} - -file.script.error=\ - script error in file {0} : {1} - -file.not.found=\ - script file {0} is not found - -engine.not.found=\ - script engine for language {0} can not be found - -engine.info=\ - Language {0} {1} implementation "{2}" {3} - -encoding.unsupported=\ - encoding {0} is not supported - -main.usage=\ -Usage: {0} [options] [arguments...]\n\ -\n\ -where [options] include:\n\ -\ \-classpath Specify where to find user class files \n\ -\ \-cp Specify where to find user class files \n\ -\ \-D= Set a system property \n\ -\ \-J Pass directly to the runtime system \n\ -\ \-l Use specified scripting language \n\ -\ \-e """, """ const pathtoroot = "./"; - loadScripts(document, 'script');""", + loadScripts(); + initTheme();""", "
    ", """
  • Search
  • """, diff --git a/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java b/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java index 413de03d23b..26030629738 100644 --- a/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java +++ b/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,7 +23,7 @@ /* * @test - * @bug 8266666 8275788 8276964 8299080 + * @bug 8266666 8275788 8276964 8299080 8276966 * @summary Implementation for snippets * @library /tools/lib ../../lib * @modules jdk.compiler/com.sun.tools.javac.api @@ -2166,6 +2166,7 @@ public class TestSnippetTag extends SnippetTester { Hello, Snippet! ----------------- external ----------------- Hello, Snippet!...more + --------------------------------------------
    """); @@ -2207,8 +2208,16 @@ public class TestSnippetTag extends SnippetTester { "pkg"); checkExit(Exit.ERROR); checkOutput(Output.OUT, true, - """ - A.java:4: error: contents mismatch"""); + """ + A.java:4: error: contents mismatch""", + """ + ----------------- inline ------------------- + Hello, Snippet! ...more + \s\s + ----------------- external ----------------- + Hello, Snippet! + \s\s + --------------------------------------------"""); checkOutput("pkg/A.html", true, """
    invalid @snippet @@ -2219,6 +2228,7 @@ public class TestSnippetTag extends SnippetTester { ----------------- external ----------------- Hello, Snippet! + --------------------------------------------
    """); diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 19f7b89a1b3..7907fa4d027 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -941,4 +941,14 @@ public class CompletionSuggestionTest extends KullaTesting { assertEval("import static java.lang.annotation.RetentionPolicy.*;"); assertCompletion("@AnnA(C|", true, "CLASS"); } + + @Test + public void testMultiSnippet() { + assertCompletion("String s = \"\"; s.len|", true, "length()"); + assertCompletion("String s() { return \"\"; } s().len|", true, "length()"); + assertCompletion("String s() { return \"\"; } import java.util.List; List.o|", true, "of("); + assertCompletion("String s() { return \"\"; } import java.ut| ", true, "util."); + assertCompletion("class S { public int length() { return 0; } } new S().len|", true, "length()"); + assertSignature("void f() { } f(|", "void f()"); + } } diff --git a/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java b/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java index 422c6c18b75..130913df8d4 100644 --- a/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java +++ b/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java @@ -47,7 +47,7 @@ public class JdiBadOptionListenExecutionControlTest { // turn on logging of launch failures Logger.getLogger("jdk.jshell.execution").setLevel(Level.ALL); JShell.builder() - .executionEngine("jdi") + .executionEngine(Presets.TEST_JDI_EXECUTION) .remoteVMOptions("-BadBadOption") .build(); } catch (IllegalStateException ex) { diff --git a/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java b/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java index 3cc2a9785a3..a941845ba49 100644 --- a/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java +++ b/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java @@ -39,6 +39,6 @@ public class JdiListeningExecutionControlTest extends ExecutionControlTestBase { @BeforeEach @Override public void setUp() { - setUp(builder -> builder.executionEngine("jdi")); + setUp(builder -> builder.executionEngine(Presets.TEST_JDI_EXECUTION)); } } diff --git a/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java b/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java new file mode 100644 index 00000000000..682c1814c66 --- /dev/null +++ b/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8366926 + * @summary Verify the instrumenation class hierarchy resolution works properly in local execution mode + * @library /tools/lib + * @modules + * jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * @build KullaTesting + * @run junit/othervm LocalExecutionInstrumentationCHRTest + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class LocalExecutionInstrumentationCHRTest extends ReplToolTesting { + + @Test + public void verifyMyClassFoundOnClassPath() { + test(new String[] { "--execution", "local" }, + a -> assertCommand(a, "public interface TestInterface {}", "| created interface TestInterface"), + a -> assertCommand(a, + "public class TestClass {" + + "public TestInterface foo(boolean b) {" + + "TestInterface test; " + + "if (b) {" + + "test = new TestInterfaceImpl1();" + + "} else {" + + "test = new TestInterfaceImpl2();" + + "}" + + "return test;" + + "}" + + "private class TestInterfaceImpl1 implements TestInterface {}" + + "private class TestInterfaceImpl2 implements TestInterface {}" + + "}", "| created class TestClass"), + a -> assertCommand(a, "new TestClass().foo(true).getClass();", "$3 ==> class TestClass$TestInterfaceImpl1"), + a -> assertCommand(a, "new TestClass().foo(false).getClass();", "$4 ==> class TestClass$TestInterfaceImpl2") + ); + } +} diff --git a/test/langtools/jdk/jshell/Presets.java b/test/langtools/jdk/jshell/Presets.java index b9a93c967dc..afe193ee5c3 100644 --- a/test/langtools/jdk/jshell/Presets.java +++ b/test/langtools/jdk/jshell/Presets.java @@ -25,16 +25,23 @@ import java.net.InetAddress; import java.util.*; public class Presets { + private static final int TEST_TIMEOUT = 20_000; + public static final String TEST_DEFAULT_EXECUTION; public static final String TEST_STANDARD_EXECUTION; + public static final String TEST_JDI_EXECUTION; static { String loopback = InetAddress.getLoopbackAddress().getHostAddress(); TEST_DEFAULT_EXECUTION = "failover:0(jdi:hostname(" + loopback + "))," + - "1(jdi:launch(true)), 2(jdi), 3(local)"; + "1(jdi:launch(true))," + + "2(jdi:timeout(" + TEST_TIMEOUT + "))," + + "3(local)"; TEST_STANDARD_EXECUTION = "failover:0(jdi:hostname(" + loopback + "))," + - "1(jdi:launch(true)), 2(jdi)"; + "1(jdi:launch(true))," + + "2(jdi:timeout(" + TEST_TIMEOUT + "))"; + TEST_JDI_EXECUTION = "jdi:timeout(" + TEST_TIMEOUT + ")"; } public static String[] addExecutionIfMissing(String[] args) { diff --git a/test/langtools/tools/javac/HexThree.java b/test/langtools/tools/javac/HexThree.java index a89db6e99f8..81c69a4f532 100644 --- a/test/langtools/tools/javac/HexThree.java +++ b/test/langtools/tools/javac/HexThree.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4920023 * @summary Test hex floating-point literals - * @author darcy */ public class HexThree { diff --git a/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java b/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java index 160c3a3093d..58dfc69ee2c 100644 --- a/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java +++ b/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java @@ -4,7 +4,6 @@ * @summary Positive tests for strings in switch with few alternatives. * @compile OneCaseSwitches.java * @run main OneCaseSwitches - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java b/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java index 2e6737e0c05..cd775a1bcb3 100644 --- a/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java +++ b/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6827009 7071246 * @summary Positive tests for strings in switch. - * @author Joseph D. Darcy */ public class StringSwitches { diff --git a/test/langtools/tools/javac/TryWithResources/BadTwr.java b/test/langtools/tools/javac/TryWithResources/BadTwr.java index 6043c527af1..a38300b7036 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwr.java +++ b/test/langtools/tools/javac/TryWithResources/BadTwr.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Verify bad TWRs don't compile * @compile/fail/ref=BadTwr.out -XDrawDiagnostics BadTwr.java */ diff --git a/test/langtools/tools/javac/TryWithResources/BadTwr.out b/test/langtools/tools/javac/TryWithResources/BadTwr.out index ed8c2524c1b..b4e951305a5 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwr.out +++ b/test/langtools/tools/javac/TryWithResources/BadTwr.out @@ -1,5 +1,5 @@ -BadTwr.java:12:46: compiler.err.already.defined: kindname.variable, r1, kindname.method, meth(java.lang.String...) -BadTwr.java:17:20: compiler.err.already.defined: kindname.variable, args, kindname.method, meth(java.lang.String...) -BadTwr.java:20:13: compiler.err.cant.assign.val.to.var: final, thatsIt -BadTwr.java:25:24: compiler.err.already.defined: kindname.variable, name, kindname.method, meth(java.lang.String...) +BadTwr.java:11:46: compiler.err.already.defined: kindname.variable, r1, kindname.method, meth(java.lang.String...) +BadTwr.java:16:20: compiler.err.already.defined: kindname.variable, args, kindname.method, meth(java.lang.String...) +BadTwr.java:19:13: compiler.err.cant.assign.val.to.var: final, thatsIt +BadTwr.java:24:24: compiler.err.already.defined: kindname.variable, name, kindname.method, meth(java.lang.String...) 4 errors diff --git a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java index 988249cef20..fc31895315c 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java +++ b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Verify bad TWRs don't compile * @compile/fail/ref=BadTwrSyntax.out -XDrawDiagnostics BadTwrSyntax.java */ diff --git a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out index 0d7263710c5..44b283dc611 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out +++ b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out @@ -1,2 +1,2 @@ -BadTwrSyntax.java:13:43: compiler.err.illegal.start.of.expr +BadTwrSyntax.java:12:43: compiler.err.illegal.start.of.expr 1 error diff --git a/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java b/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java index e77de854f47..472b9089c1e 100644 --- a/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java +++ b/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,6 @@ /* * @test * @bug 7013420 - * @author Joseph D. Darcy * @summary Test that resource variables are accepted as explicitly final. */ diff --git a/test/langtools/tools/javac/TryWithResources/PlainTry.java b/test/langtools/tools/javac/TryWithResources/PlainTry.java index edbb233578b..52c05a1ff7c 100644 --- a/test/langtools/tools/javac/TryWithResources/PlainTry.java +++ b/test/langtools/tools/javac/TryWithResources/PlainTry.java @@ -1,9 +1,8 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Test error messages for an unadorned try - * @compile/fail/ref=PlainTry.out -XDrawDiagnostics PlainTry.java + * @compile/fail/ref=PlainTry.out -XDrawDiagnostics PlainTry.java */ public class PlainTry { public static void meth() { diff --git a/test/langtools/tools/javac/TryWithResources/PlainTry.out b/test/langtools/tools/javac/TryWithResources/PlainTry.out index 9357cd8cdd7..0f8f16ae1e8 100644 --- a/test/langtools/tools/javac/TryWithResources/PlainTry.out +++ b/test/langtools/tools/javac/TryWithResources/PlainTry.out @@ -1,2 +1,2 @@ -PlainTry.java:10:9: compiler.err.try.without.catch.finally.or.resource.decls +PlainTry.java:9:9: compiler.err.try.without.catch.finally.or.resource.decls 1 error diff --git a/test/langtools/tools/javac/TryWithResources/TwrFlow.java b/test/langtools/tools/javac/TryWithResources/TwrFlow.java index 8ea54244570..366648fb42e 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrFlow.java +++ b/test/langtools/tools/javac/TryWithResources/TwrFlow.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 7013420 - * @author Joseph D. Darcy * @summary Test exception analysis of try-with-resources blocks * @compile/fail/ref=TwrFlow.out -XDrawDiagnostics TwrFlow.java */ diff --git a/test/langtools/tools/javac/TryWithResources/TwrFlow.out b/test/langtools/tools/javac/TryWithResources/TwrFlow.out index 23db6517c43..758fb778aae 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrFlow.out +++ b/test/langtools/tools/javac/TryWithResources/TwrFlow.out @@ -1,3 +1,3 @@ -TwrFlow.java:14:11: compiler.err.except.never.thrown.in.try: java.io.IOException -TwrFlow.java:12:21: compiler.err.unreported.exception.implicit.close: CustomCloseException, twrFlow +TwrFlow.java:13:11: compiler.err.except.never.thrown.in.try: java.io.IOException +TwrFlow.java:11:21: compiler.err.unreported.exception.implicit.close: CustomCloseException, twrFlow 2 errors diff --git a/test/langtools/tools/javac/TryWithResources/TwrLint.java b/test/langtools/tools/javac/TryWithResources/TwrLint.java index 39c5ed67f8f..ca45efc1e0a 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrLint.java +++ b/test/langtools/tools/javac/TryWithResources/TwrLint.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 6965277 6967065 - * @author Joseph D. Darcy * @summary Check that -Xlint:twr warnings are generated as expected * @compile/ref=TwrLint.out -Xlint:try,deprecation -XDrawDiagnostics TwrLint.java */ diff --git a/test/langtools/tools/javac/TryWithResources/TwrLint.out b/test/langtools/tools/javac/TryWithResources/TwrLint.out index 6adfc5122b3..28778b0e3a5 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrLint.out +++ b/test/langtools/tools/javac/TryWithResources/TwrLint.out @@ -1,3 +1,3 @@ -TwrLint.java:14:15: compiler.warn.try.explicit.close.call -TwrLint.java:13:21: compiler.warn.try.resource.not.referenced: r3 +TwrLint.java:13:15: compiler.warn.try.explicit.close.call +TwrLint.java:12:21: compiler.warn.try.resource.not.referenced: r3 2 warnings diff --git a/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java b/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java index dbbb633efe1..002e30277e3 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java +++ b/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,6 @@ /* * @test * @bug 6911256 6964740 7013420 - * @author Joseph D. Darcy * @summary Test that TWR and multi-catch play well together * @compile TwrMultiCatch.java * @run main TwrMultiCatch diff --git a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java index ac0a72645ba..0d6c2f4bc44 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java +++ b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 7013420 - * @author Joseph D. Darcy * @summary Verify invalid TWR block is not accepted. * @compile/fail/ref=TwrOnNonResource.out -XDrawDiagnostics TwrOnNonResource.java */ diff --git a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out index 07488a78071..9a01e28e105 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out +++ b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out @@ -1,4 +1,4 @@ -TwrOnNonResource.java:11:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) -TwrOnNonResource.java:14:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) -TwrOnNonResource.java:17:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) +TwrOnNonResource.java:10:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) +TwrOnNonResource.java:13:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) +TwrOnNonResource.java:16:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) 3 errors diff --git a/test/langtools/tools/javac/TryWithResources/TwrSuppression.java b/test/langtools/tools/javac/TryWithResources/TwrSuppression.java index 9834d5a17b2..0c511d28cbc 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrSuppression.java +++ b/test/langtools/tools/javac/TryWithResources/TwrSuppression.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,6 @@ /* * @test * @bug 6971877 - * @author Joseph D. Darcy * @summary Verify a primary exception suppresses all throwables */ diff --git a/test/langtools/tools/javac/TryWithResources/WeirdTwr.java b/test/langtools/tools/javac/TryWithResources/WeirdTwr.java index 6331507097c..fbfbbe6ecd8 100644 --- a/test/langtools/tools/javac/TryWithResources/WeirdTwr.java +++ b/test/langtools/tools/javac/TryWithResources/WeirdTwr.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Strange TWRs * @compile WeirdTwr.java * @run main WeirdTwr diff --git a/test/langtools/tools/javac/annotations/pos/TrailingComma.java b/test/langtools/tools/javac/annotations/pos/TrailingComma.java index d6601ce419d..0fa4bdb3546 100644 --- a/test/langtools/tools/javac/annotations/pos/TrailingComma.java +++ b/test/langtools/tools/javac/annotations/pos/TrailingComma.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6337964 * @summary javac incorrectly disallows trailing comma in annotation arrays - * @author darcy * @compile TrailingComma.java */ diff --git a/test/langtools/tools/javac/boxing/BoxingCaching.java b/test/langtools/tools/javac/boxing/BoxingCaching.java index 2d7acb817f0..f4c1337f240 100644 --- a/test/langtools/tools/javac/boxing/BoxingCaching.java +++ b/test/langtools/tools/javac/boxing/BoxingCaching.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4990346 8200478 * @summary Verify autoboxed values are cached as required. - * @author Joseph D. Darcy */ public class BoxingCaching { diff --git a/test/langtools/tools/javac/enum/6350057/T6350057.java b/test/langtools/tools/javac/enum/6350057/T6350057.java index ff96b710af6..8eb0aa997b6 100644 --- a/test/langtools/tools/javac/enum/6350057/T6350057.java +++ b/test/langtools/tools/javac/enum/6350057/T6350057.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6350057 7025809 * @summary Test that parameters on implicit enum methods have the right kind - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/enum/AbstractEmptyEnum.java b/test/langtools/tools/javac/enum/AbstractEmptyEnum.java index 38c13bde828..7f5792167e8 100644 --- a/test/langtools/tools/javac/enum/AbstractEmptyEnum.java +++ b/test/langtools/tools/javac/enum/AbstractEmptyEnum.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary empty enum cannot be abstract - * @author Joseph D. Darcy * * @compile/fail/ref=AbstractEmptyEnum.out -XDrawDiagnostics AbstractEmptyEnum.java */ diff --git a/test/langtools/tools/javac/enum/AbstractEmptyEnum.out b/test/langtools/tools/javac/enum/AbstractEmptyEnum.out index 933363a3fe6..b6b2460bb68 100644 --- a/test/langtools/tools/javac/enum/AbstractEmptyEnum.out +++ b/test/langtools/tools/javac/enum/AbstractEmptyEnum.out @@ -1,2 +1,2 @@ -AbstractEmptyEnum.java:10:8: compiler.err.does.not.override.abstract: AbstractEmptyEnum, m(), AbstractEmptyEnum +AbstractEmptyEnum.java:9:8: compiler.err.does.not.override.abstract: AbstractEmptyEnum, m(), AbstractEmptyEnum 1 error diff --git a/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java b/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java index 36c090668c4..8ed9e148978 100644 --- a/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java +++ b/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5009601 5010455 5005748 * @summary enum constructors can be declared private - * @author Joseph D. Darcy */ import java.util.*; diff --git a/test/langtools/tools/javac/enum/EnumPrivateConstructor.java b/test/langtools/tools/javac/enum/EnumPrivateConstructor.java index 84a28a81976..412f79945c8 100644 --- a/test/langtools/tools/javac/enum/EnumPrivateConstructor.java +++ b/test/langtools/tools/javac/enum/EnumPrivateConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5009601 * @summary enum constructors can be declared private - * @author Joseph D. Darcy * * @compile EnumPrivateConstructor.java */ diff --git a/test/langtools/tools/javac/enum/EnumProtectedConstructor.java b/test/langtools/tools/javac/enum/EnumProtectedConstructor.java index 481de39319c..298dcc738c9 100644 --- a/test/langtools/tools/javac/enum/EnumProtectedConstructor.java +++ b/test/langtools/tools/javac/enum/EnumProtectedConstructor.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum constructors cannot be declared public or protected - * @author Joseph D. Darcy * * @compile/fail/ref=EnumProtectedConstructor.out -XDrawDiagnostics EnumProtectedConstructor.java */ diff --git a/test/langtools/tools/javac/enum/EnumProtectedConstructor.out b/test/langtools/tools/javac/enum/EnumProtectedConstructor.out index 71826a2d331..5cc734020ac 100644 --- a/test/langtools/tools/javac/enum/EnumProtectedConstructor.out +++ b/test/langtools/tools/javac/enum/EnumProtectedConstructor.out @@ -1,2 +1,2 @@ -EnumProtectedConstructor.java:16:15: compiler.err.mod.not.allowed.here: protected +EnumProtectedConstructor.java:15:15: compiler.err.mod.not.allowed.here: protected 1 error diff --git a/test/langtools/tools/javac/enum/EnumPublicConstructor.java b/test/langtools/tools/javac/enum/EnumPublicConstructor.java index 31360597035..b5c622208ef 100644 --- a/test/langtools/tools/javac/enum/EnumPublicConstructor.java +++ b/test/langtools/tools/javac/enum/EnumPublicConstructor.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum constructors cannot be declared public or protected - * @author Joseph D. Darcy * * @compile/fail/ref=EnumPublicConstructor.out -XDrawDiagnostics EnumPublicConstructor.java */ diff --git a/test/langtools/tools/javac/enum/EnumPublicConstructor.out b/test/langtools/tools/javac/enum/EnumPublicConstructor.out index 56c099f4330..89ba98d1d81 100644 --- a/test/langtools/tools/javac/enum/EnumPublicConstructor.out +++ b/test/langtools/tools/javac/enum/EnumPublicConstructor.out @@ -1,2 +1,2 @@ -EnumPublicConstructor.java:16:12: compiler.err.mod.not.allowed.here: public +EnumPublicConstructor.java:15:12: compiler.err.mod.not.allowed.here: public 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java index 5af53768684..d04a2558715 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared abstract - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyAbstractEnum1.out -XDrawDiagnostics ExplicitlyAbstractEnum1.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out index 827e331a491..902c9e5730f 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out @@ -1,2 +1,2 @@ -ExplicitlyAbstractEnum1.java:9:10: compiler.err.mod.not.allowed.here: abstract +ExplicitlyAbstractEnum1.java:8:10: compiler.err.mod.not.allowed.here: abstract 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java index cbd9bd512d6..293c36a867d 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared abstract even if they are abstract - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyAbstractEnum2.out -XDrawDiagnostics ExplicitlyAbstractEnum2.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out index 8a35cec3180..f2c25ca5d4e 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out @@ -1,2 +1,2 @@ -ExplicitlyAbstractEnum2.java:9:10: compiler.err.mod.not.allowed.here: abstract +ExplicitlyAbstractEnum2.java:8:10: compiler.err.mod.not.allowed.here: abstract 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java index 1dba3d0dff5..1e2bf650ca5 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared final even if they are - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyFinalEnum1.out -XDrawDiagnostics ExplicitlyFinalEnum1.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out index 023d00e265f..11bbdb690b6 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out @@ -1,2 +1,2 @@ -ExplicitlyFinalEnum1.java:9:7: compiler.err.mod.not.allowed.here: final +ExplicitlyFinalEnum1.java:8:7: compiler.err.mod.not.allowed.here: final 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java index 73d3b8acb7b..0c02799c3d3 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared final - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyFinalEnum2.out -XDrawDiagnostics ExplicitlyFinalEnum2.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out index 09cbf1461ad..8d67c888864 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out @@ -1,2 +1,2 @@ -ExplicitlyFinalEnum2.java:9:7: compiler.err.mod.not.allowed.here: final +ExplicitlyFinalEnum2.java:8:7: compiler.err.mod.not.allowed.here: final 1 error diff --git a/test/langtools/tools/javac/enum/FauxEnum1.java b/test/langtools/tools/javac/enum/FauxEnum1.java index 5ae6a8b090c..7565633032f 100644 --- a/test/langtools/tools/javac/enum/FauxEnum1.java +++ b/test/langtools/tools/javac/enum/FauxEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009574 * @summary verify java.lang.Enum can't be directly subclassed - * @author Joseph D. Darcy * * @compile/fail/ref=FauxEnum1.out -XDrawDiagnostics FauxEnum1.java */ diff --git a/test/langtools/tools/javac/enum/FauxEnum1.out b/test/langtools/tools/javac/enum/FauxEnum1.out index ff1e8fa7315..7cae5443e25 100644 --- a/test/langtools/tools/javac/enum/FauxEnum1.out +++ b/test/langtools/tools/javac/enum/FauxEnum1.out @@ -1,4 +1,4 @@ -FauxEnum1.java:10:8: compiler.err.enum.no.subclassing +FauxEnum1.java:9:8: compiler.err.enum.no.subclassing - compiler.note.unchecked.filename: FauxEnum1.java - compiler.note.unchecked.recompile 1 error diff --git a/test/langtools/tools/javac/enum/FauxEnum3.java b/test/langtools/tools/javac/enum/FauxEnum3.java index ec53aeb5951..6cf41cfa889 100644 --- a/test/langtools/tools/javac/enum/FauxEnum3.java +++ b/test/langtools/tools/javac/enum/FauxEnum3.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009574 * @summary verify an enum type can't be directly subclassed - * @author Joseph D. Darcy * * @compile/fail/ref=FauxEnum3.out -XDrawDiagnostics FauxEnum3.java */ diff --git a/test/langtools/tools/javac/enum/FauxEnum3.out b/test/langtools/tools/javac/enum/FauxEnum3.out index 885154d073a..33596c87b51 100644 --- a/test/langtools/tools/javac/enum/FauxEnum3.out +++ b/test/langtools/tools/javac/enum/FauxEnum3.out @@ -1,2 +1,2 @@ -FauxEnum3.java:10:14: compiler.err.enum.types.not.extensible +FauxEnum3.java:9:14: compiler.err.enum.types.not.extensible 1 error diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum1.java b/test/langtools/tools/javac/enum/FauxSpecialEnum1.java index 5b427bb110d..578864061d0 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum1.java +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary verify specialized enum classes can't be abstract - * @author Joseph D. Darcy * * @compile/fail/ref=FauxSpecialEnum1.out -XDrawDiagnostics FauxSpecialEnum1.java */ diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum1.out b/test/langtools/tools/javac/enum/FauxSpecialEnum1.out index 4530bf40a88..ed9a5feaf1f 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum1.out +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum1.out @@ -1,2 +1,2 @@ -FauxSpecialEnum1.java:14:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum1$2, test(), compiler.misc.anonymous.class: FauxSpecialEnum1$2 +FauxSpecialEnum1.java:13:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum1$2, test(), compiler.misc.anonymous.class: FauxSpecialEnum1$2 1 error diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum2.java b/test/langtools/tools/javac/enum/FauxSpecialEnum2.java index 5c0cc610fa2..6bf9a97f29c 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum2.java +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum2.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary verify specialized enum classes can't be abstract - * @author Joseph D. Darcy * * @compile/fail/ref=FauxSpecialEnum2.out -XDrawDiagnostics FauxSpecialEnum2.java */ diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum2.out b/test/langtools/tools/javac/enum/FauxSpecialEnum2.out index 81de8f41726..aa1cbc8e9cc 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum2.out +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum2.out @@ -1,2 +1,2 @@ -FauxSpecialEnum2.java:12:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum2$1, test(), compiler.misc.anonymous.class: FauxSpecialEnum2$1 +FauxSpecialEnum2.java:11:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum2$1, test(), compiler.misc.anonymous.class: FauxSpecialEnum2$1 1 error diff --git a/test/langtools/tools/javac/file/FSInfoTest.java b/test/langtools/tools/javac/file/FSInfoTest.java index bbd9da9bc97..e9b0c9f55bf 100644 --- a/test/langtools/tools/javac/file/FSInfoTest.java +++ b/test/langtools/tools/javac/file/FSInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,7 +23,6 @@ import com.sun.tools.javac.file.FSInfo; import com.sun.tools.javac.util.Context; -import org.testng.annotations.Test; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +30,7 @@ import java.nio.file.Path; import java.util.Locale; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; /* * @test @@ -38,7 +38,7 @@ import java.util.jar.Manifest; * @summary Test com.sun.tools.javac.file.FSInfo * @modules jdk.compiler/com.sun.tools.javac.util * jdk.compiler/com.sun.tools.javac.file - * @run testng FSInfoTest + * @run junit FSInfoTest */ public class FSInfoTest { diff --git a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java index 54519aa6afa..291e8386fe3 100644 --- a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java +++ b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,14 +30,9 @@ * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * @build toolbox.ToolBox - * @run testng MultiReleaseJarAwareSJFM + * @run junit MultiReleaseJarAwareSJFM */ -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; import javax.tools.FileObject; import javax.tools.JavaFileManager; @@ -50,11 +45,18 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import toolbox.JarTask; import toolbox.JavacTask; import toolbox.ToolBox; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MultiReleaseJarAwareSJFM { private static final int CURRENT_VERSION = Runtime.version().major(); @@ -113,7 +115,7 @@ public class MultiReleaseJarAwareSJFM { } }; - @BeforeClass + @BeforeAll public void setup() throws Exception { tb.createDirectories("classes", "classes/META-INF/versions/9", @@ -140,7 +142,7 @@ public class MultiReleaseJarAwareSJFM { .run(); } - @AfterClass + @AfterAll public void teardown() throws Exception { tb.deleteFiles( "classes/META-INF/versions/" + CURRENT_VERSION + "/version/Version.class", @@ -159,7 +161,6 @@ public class MultiReleaseJarAwareSJFM { ); } - @DataProvider(name = "versions") public Object[][] data() { return new Object[][] { {"", 8}, @@ -169,7 +170,8 @@ public class MultiReleaseJarAwareSJFM { }; } - @Test(dataProvider = "versions") + @ParameterizedTest + @MethodSource("data") public void test(String version, int expected) throws Throwable { StandardJavaFileManager jfm = ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null); jfm.setLocation(jloc, List.of(new File("multi-release.jar"))); @@ -183,7 +185,7 @@ public class MultiReleaseJarAwareSJFM { MethodType mt = MethodType.methodType(int.class); MethodHandle mh = MethodHandles.lookup().findVirtual(versionClass, "getVersion", mt); int v = (int)mh.invoke(versionClass.newInstance()); - Assert.assertEquals(v, expected); + Assertions.assertEquals(expected, v); jfm.close(); } diff --git a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java index 09209e14c9d..1c1704510ca 100644 --- a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java +++ b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java @@ -30,13 +30,14 @@ * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask - * @run testng/timeout=480 MultiReleaseJarTest + * @run junit/timeout=480 MultiReleaseJarTest */ -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import toolbox.JarTask; import toolbox.JavacTask; @@ -44,6 +45,7 @@ import toolbox.Task; import toolbox.ToolBox; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MultiReleaseJarTest { private final String main1 = @@ -84,7 +86,7 @@ public class MultiReleaseJarTest { private final ToolBox tb = new ToolBox(); - @BeforeClass + @BeforeAll public void setup() throws Exception { tb.createDirectories("classes", "classes/META-INF/versions/9"); new JavacTask(tb) @@ -111,7 +113,7 @@ public class MultiReleaseJarTest { ); } - @AfterClass + @AfterAll public void teardown() throws Exception { tb.deleteFiles( "multi-release.jar", @@ -120,8 +122,9 @@ public class MultiReleaseJarTest { ); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main1Runtime(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -134,8 +137,9 @@ public class MultiReleaseJarTest { } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 8 -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main1Release8(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -148,8 +152,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 9 -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main1Release9(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -162,8 +167,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main2Runtime(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -176,8 +182,9 @@ public class MultiReleaseJarTest { } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 8 -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main2Release8(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -190,8 +197,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 9 -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main2Release9(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -204,7 +212,6 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @DataProvider(name="modes") public Object[][] createModes() { return new Object[][] { new Object[] {Task.Mode.API}, diff --git a/test/langtools/tools/javac/generics/InheritanceConflict3.java b/test/langtools/tools/javac/generics/InheritanceConflict3.java index ae725d6c1e2..d9f3c76b06e 100644 --- a/test/langtools/tools/javac/generics/InheritanceConflict3.java +++ b/test/langtools/tools/javac/generics/InheritanceConflict3.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 4984158 * @summary two inherited methods with same signature - * @author darcy * * @compile/fail/ref=InheritanceConflict3.out -XDrawDiagnostics InheritanceConflict3.java */ diff --git a/test/langtools/tools/javac/generics/InheritanceConflict3.out b/test/langtools/tools/javac/generics/InheritanceConflict3.out index 60cfe5d2148..c168d4b2b86 100644 --- a/test/langtools/tools/javac/generics/InheritanceConflict3.out +++ b/test/langtools/tools/javac/generics/InheritanceConflict3.out @@ -1,3 +1,3 @@ -InheritanceConflict3.java:14:10: compiler.err.name.clash.same.erasure: f(java.lang.Object), f(T) -InheritanceConflict3.java:17:1: compiler.err.concrete.inheritance.conflict: f(java.lang.Object), inheritance.conflict3.X1, f(T), inheritance.conflict3.X1, inheritance.conflict3.X1 +InheritanceConflict3.java:13:10: compiler.err.name.clash.same.erasure: f(java.lang.Object), f(T) +InheritanceConflict3.java:16:1: compiler.err.concrete.inheritance.conflict: f(java.lang.Object), inheritance.conflict3.X1, f(T), inheritance.conflict3.X1, inheritance.conflict3.X1 2 errors diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java b/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java index 91ba372bcb3..1a560125e23 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,11 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng InInterface + * @run junit InInterface */ -import static org.testng.Assert.assertEquals; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; interface LTII { @@ -51,12 +51,12 @@ interface LTII { } -@Test public class InInterface implements LTII { + @Test public void testLambdaInDefaultMethod() { - assertEquals(t1().m(), "yo"); - assertEquals(t2().m("p"), "snurp"); + assertEquals("yo", t1().m()); + assertEquals("snurp", t2().m("p")); } } diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java b/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java index d5282afc9df..c8322668059 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng InnerConstructor + * @run junit InnerConstructor */ -import static org.testng.Assert.assertEquals; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class InnerConstructor { + @Test public void testLambdaWithInnerConstructor() { - assertEquals(seq1().m().toString(), "Cbl:nada"); - assertEquals(seq2().m("rats").toString(), "Cbl:rats"); + assertEquals("Cbl:nada", seq1().m().toString()); + assertEquals("Cbl:rats", seq2().m("rats").toString()); } Ib1 seq1() { diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java index b207fd1a7d5..fb602badb25 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,14 +25,12 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest1 + * @run junit/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest1 */ -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -import static org.testng.Assert.assertEquals; - -@Test public class LambdaTranslationTest1 extends LT1Sub { String cntxt = "blah"; @@ -43,7 +41,7 @@ public class LambdaTranslationTest1 extends LT1Sub { private static void appendResult(Object s) { result.set(result.get().toString() + s); } private static void assertResult(String expected) { - assertEquals(result.get().toString(), expected); + assertEquals(expected, result.get().toString()); } static Integer count(String s) { @@ -66,6 +64,7 @@ public class LambdaTranslationTest1 extends LT1Sub { setResult(String.format("d:%f", d)); } + @Test public void testLambdas() { TBlock b = t -> {setResult("Sink0::" + t);}; b.apply("Howdy"); @@ -127,6 +126,7 @@ public class LambdaTranslationTest1 extends LT1Sub { assertResult("b11: *999*"); } + @Test public void testMethodRefs() { LT1IA ia = LambdaTranslationTest1::eye; ia.doit(1234); @@ -147,6 +147,7 @@ public class LambdaTranslationTest1 extends LT1Sub { assertEquals((Integer) 6, a.doit("shower")); } + @Test public void testInner() throws Exception { (new In()).doInner(); } diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java index e7e484730b2..7eec78b55d2 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,26 +25,26 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest2 + * @run junit/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest2 */ -import org.testng.annotations.Test; import java.util.ArrayList; import java.util.List; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; /** * LambdaTranslationTest2 -- end-to-end smoke tests for lambda evaluation */ -@Test public class LambdaTranslationTest2 { final String dummy = "dummy"; + @Test public void testLambdas() { TPredicate isEmpty = s -> s.isEmpty(); assertTrue(isEmpty.test("")); @@ -100,6 +100,7 @@ public class LambdaTranslationTest2 { String make(); } + @Test public void testBridges() { Factory of = () -> "y"; Factory ef = () -> "z"; @@ -112,6 +113,7 @@ public class LambdaTranslationTest2 { assertEquals("z", ((Factory) ef).make()); } + @Test public void testBridgesImplicitSpecialization() { StringFactory sf = () -> "x"; @@ -121,6 +123,7 @@ public class LambdaTranslationTest2 { assertEquals("x", ((Factory) sf).make()); } + @Test public void testBridgesExplicitSpecialization() { StringFactory2 sf = () -> "x"; @@ -130,6 +133,7 @@ public class LambdaTranslationTest2 { assertEquals("x", ((Factory) sf).make()); } + @Test public void testSuperCapture() { class A { String make() { return "x"; } @@ -201,6 +205,7 @@ public class LambdaTranslationTest2 { return String.format("f%f d%f", a0, a1); } + @Test public void testPrimitiveWidening() { WidenS ws1 = LambdaTranslationTest2::pwS1; assertEquals("b1 s2", ws1.m((byte) 1, (short) 2)); @@ -225,11 +230,13 @@ public class LambdaTranslationTest2 { return String.format("b%d s%d c%c i%d j%d z%b f%f d%f", a0, a1, a2, a3, a4, a5, a6, a7); } + @Test public void testUnboxing() { Unbox u = LambdaTranslationTest2::pu; assertEquals("b1 s2 cA i4 j5 ztrue f6.000000 d7.000000", u.m((byte)1, (short) 2, 'A', 4, 5L, true, 6.0f, 7.0)); } + @Test public void testBoxing() { Box b = LambdaTranslationTest2::pb; assertEquals("b1 s2 cA i4 j5 ztrue f6.000000 d7.000000", b.m((byte) 1, (short) 2, 'A', 4, 5L, true, 6.0f, 7.0)); @@ -239,6 +246,7 @@ public class LambdaTranslationTest2 { return ((String) o).equals("foo"); } + @Test public void testArgCastingAdaptation() { TPredicate p = LambdaTranslationTest2::cc; assertTrue(p.test("foo")); @@ -247,12 +255,14 @@ public class LambdaTranslationTest2 { interface SonOfPredicate extends TPredicate { } + @Test public void testExtendsSAM() { SonOfPredicate p = s -> s.isEmpty(); assertTrue(p.test("")); assertTrue(!p.test("foo")); } + @Test public void testConstructorRef() { Factory> lf = ArrayList::new; List list = lf.make(); @@ -266,6 +276,7 @@ public class LambdaTranslationTest2 { return "private"; } + @Test public void testPrivateMethodRef() { Factory sf = LambdaTranslationTest2::privateMethod; assertEquals("private", sf.make()); @@ -275,6 +286,7 @@ public class LambdaTranslationTest2 { String make(); } + @Test public void testPrivateIntf() { PrivateIntf p = () -> "foo"; assertEquals("foo", p.make()); @@ -284,11 +296,12 @@ public class LambdaTranslationTest2 { public T op(T a, T b); } + @Test public void testBoxToObject() { Op maxer = Math::max; for (int i=-100000; i < 100000; i += 100) for (int j=-100000; j < 100000; j += 99) { - assertEquals((int) maxer.op(i,j), Math.max(i,j)); + assertEquals(Math.max(i,j), (int) maxer.op(i,j)); } } @@ -296,6 +309,7 @@ public class LambdaTranslationTest2 { return "protected"; } + @Test public void testProtectedMethodRef() { Factory sf = LambdaTranslationTest2::protectedMethod; assertEquals("protected", sf.make()); @@ -331,6 +345,7 @@ public class LambdaTranslationTest2 { } } + @Test public void testInnerClassMethodRef() { Factory fs = new Inner1()::m1; assertEquals("Inner1.m1()", fs.make()); diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java index a0ecb7c370c..9238b1d2e4e 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,27 +25,26 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestFDCCE + * @run junit MethodReferenceTestFDCCE */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; /** * Method references and raw types. * @author Robert Field */ -@Test @SuppressWarnings({"rawtypes", "unchecked"}) public class MethodReferenceTestFDCCE { static void assertCCE(Throwable t) { - assertEquals(t.getClass().getName(), "java.lang.ClassCastException"); + assertEquals("java.lang.ClassCastException", t.getClass().getName()); } interface Pred { boolean accept(T x); } @@ -79,18 +78,21 @@ public class MethodReferenceTestFDCCE { return 123; } + @Test public void testMethodReferenceFDPrim1() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; assertTrue(p2.accept((Byte)(byte)15)); } + @Test public void testMethodReferenceFDPrim2() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; assertTrue(p2.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimICCE() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; @@ -102,6 +104,7 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDPrimOCCE() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; @@ -113,12 +116,14 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDRef() { Pred p = MethodReferenceTestFDCCE::tst; Pred p2 = p; assertTrue(p2.accept(new B())); } + @Test public void testMethodReferenceFDRefCCE() { Pred p = MethodReferenceTestFDCCE::tst; Pred p2 = p; @@ -130,23 +135,27 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDPrimPrim() { Ps p = MethodReferenceTestFDCCE::isMinor; assertTrue(p.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimBoxed() { Ps p = MethodReferenceTestFDCCE::stst; assertTrue(p.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimRef() { Oo p = MethodReferenceTestFDCCE::otst; - assertEquals(p.too(15).getClass().getName(), "java.lang.Integer"); + assertEquals("java.lang.Integer", p.too(15).getClass().getName()); } + @Test public void testMethodReferenceFDRet1() { Reto p = MethodReferenceTestFDCCE::ritst; - assertEquals(p.m(), (Short)(short)123); + assertEquals((Short)(short)123, p.m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java index d2a9763a201..f3cce62e7e2 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerDefault + * @run junit MethodReferenceTestInnerDefault */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -60,9 +59,9 @@ interface InDefB extends InDefA { } } -@Test public class MethodReferenceTestInnerDefault implements InDefB { + @Test public void testMethodReferenceInnerDefault() { (new In()).testMethodReferenceInnerDefault(); } @@ -73,13 +72,13 @@ public class MethodReferenceTestInnerDefault implements InDefB { IDSs q; q = MethodReferenceTestInnerDefault.this::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = MethodReferenceTestInnerDefault.this::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = MethodReferenceTestInnerDefault.this::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java index 76a82891c3a..326f8057a76 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,24 +25,24 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerInstance + * @run junit MethodReferenceTestInnerInstance */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestInnerInstance { + @Test public void testMethodReferenceInnerInstance() { cia().cib().testMethodReferenceInstance(); } + @Test public void testMethodReferenceInnerExternal() { cia().cib().testMethodReferenceExternal(); } @@ -63,14 +63,14 @@ public class MethodReferenceTestInnerInstance { SI q; q = CIA.this::xI; - assertEquals(q.m(55), "xI:55"); + assertEquals("xI:55", q.m(55)); } public void testMethodReferenceExternal() { SI q; q = (new E())::xI; - assertEquals(q.m(77), "ExI:77"); + assertEquals("ExI:77", q.m(77)); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java index a9cc399c27e..ef7eae72429 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,19 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerVarArgsThis + * @run junit MethodReferenceTestInnerVarArgsThis */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestInnerVarArgsThis { interface NsII { @@ -134,62 +133,62 @@ public class MethodReferenceTestInnerVarArgsThis { NsII q; q = CIA.this::xvO; - assertEquals(q.m(55, 66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55, 66)); } public void testVarArgsNsArray() { Nsai q; q = CIA.this::xvO; - assertEquals(q.m(new int[]{55, 66}), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[]{55, 66})); } public void testVarArgsNsII() { NsII q; q = CIA.this::xvI; - assertEquals(q.m(33, 7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33, 7)); q = CIA.this::xIvI; - assertEquals(q.m(50, 40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50, 40)); q = CIA.this::xvi; - assertEquals(q.m(100, 23), "xvi:123"); + assertEquals("xvi:123", q.m(100, 23)); q = CIA.this::xIvi; - assertEquals(q.m(9, 21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9, 21)); } public void testVarArgsNsiii() { Nsiii q; q = CIA.this::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = CIA.this::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = CIA.this::xvi; - assertEquals(q.m(900, 80, 7), "xvi:987"); + assertEquals("xvi:987", q.m(900, 80, 7)); q = CIA.this::xIvi; - assertEquals(q.m(333, 27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333, 27, 72)); } public void testVarArgsNsi() { Nsi q; q = CIA.this::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = CIA.this::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = CIA.this::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = CIA.this::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args @@ -197,7 +196,7 @@ public class MethodReferenceTestInnerVarArgsThis { NsaO q; q = CIA.this::xvO; - assertEquals(q.m(new String[]{"yo", "there", "dude"}), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[]{"yo", "there", "dude"})); } } @@ -218,28 +217,34 @@ public class MethodReferenceTestInnerVarArgsThis { } // These should be processed as var args + @Test public void testVarArgsNsSuperclass() { cia().cib().testVarArgsNsSuperclass(); } + @Test public void testVarArgsNsArray() { cia().cib().testVarArgsNsArray(); } + @Test public void testVarArgsNsII() { cia().cib().testVarArgsNsII(); } + @Test public void testVarArgsNsiii() { cia().cib().testVarArgsNsiii(); } + @Test public void testVarArgsNsi() { cia().cib().testVarArgsNsi(); } // These should NOT be processed as var args + @Test public void testVarArgsNsaO() { cia().cib().testVarArgsNsaO(); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java index 15b4a481995..52f650c4c52 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInstance + * @run junit MethodReferenceTestInstance */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -42,7 +41,6 @@ class MethodReferenceTestInstance_E { } } -@Test public class MethodReferenceTestInstance { interface SI { String m(Integer a); } @@ -51,18 +49,20 @@ public class MethodReferenceTestInstance { return "xI:" + i; } + @Test public void testMethodReferenceInstance() { SI q; q = this::xI; - assertEquals(q.m(55), "xI:55"); + assertEquals("xI:55", q.m(55)); } + @Test public void testMethodReferenceExternal() { SI q; q = (new MethodReferenceTestInstance_E())::xI; - assertEquals(q.m(77), "ExI:77"); + assertEquals("ExI:77", q.m(77)); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java index e67ed379d13..2b255b3da3e 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestKinds + * @run junit MethodReferenceTestKinds */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestKinds extends MethodReferenceTestKindsSup { interface S0 { String get(); } @@ -65,76 +63,86 @@ public class MethodReferenceTestKinds extends MethodReferenceTestKindsSup { static String staticMethod0() { return "SM:0"; } static String staticMethod1(MethodReferenceTestKinds x) { return "SM:1-" + x; } - MethodReferenceTestKinds(String val) { - super(val); - } - MethodReferenceTestKinds() { super("blank"); } MethodReferenceTestKinds inst(String val) { - return new MethodReferenceTestKinds(val); + var inst = new MethodReferenceTestKinds(); + inst.val = val; // simulate `MethodReferenceTestKinds(String val)` constructor + return inst; } + @Test public void testMRBound() { S0 var = this::instanceMethod0; - assertEquals(var.get(), "IM:0-MethodReferenceTestKinds(blank)"); + assertEquals("IM:0-MethodReferenceTestKinds(blank)", var.get()); } + @Test public void testMRBoundArg() { S1 var = this::instanceMethod1; - assertEquals(var.get(inst("arg")), "IM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)"); + assertEquals("IM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRUnbound() { S1 var = MethodReferenceTestKinds::instanceMethod0; - assertEquals(var.get(inst("rcvr")), "IM:0-MethodReferenceTestKinds(rcvr)"); + assertEquals("IM:0-MethodReferenceTestKinds(rcvr)", var.get(inst("rcvr"))); } + @Test public void testMRUnboundArg() { S2 var = MethodReferenceTestKinds::instanceMethod1; - assertEquals(var.get(inst("rcvr"), inst("arg")), "IM:1-MethodReferenceTestKinds(rcvr)MethodReferenceTestKinds(arg)"); + assertEquals("IM:1-MethodReferenceTestKinds(rcvr)MethodReferenceTestKinds(arg)", var.get(inst("rcvr"), inst("arg"))); } + @Test public void testMRSuper() { S0 var = super::instanceMethod0; - assertEquals(var.get(), "SIM:0-MethodReferenceTestKinds(blank)"); + assertEquals("SIM:0-MethodReferenceTestKinds(blank)", var.get()); } + @Test public void testMRSuperArg() { S1 var = super::instanceMethod1; - assertEquals(var.get(inst("arg")), "SIM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)"); + assertEquals("SIM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRStatic() { S0 var = MethodReferenceTestKinds::staticMethod0; - assertEquals(var.get(), "SM:0"); + assertEquals("SM:0", var.get()); } + @Test public void testMRStaticArg() { S1 var = MethodReferenceTestKinds::staticMethod1; - assertEquals(var.get(inst("arg")), "SM:1-MethodReferenceTestKinds(arg)"); + assertEquals("SM:1-MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRTopLevel() { SN0 var = MethodReferenceTestKindsBase::new; - assertEquals(var.make().toString(), "MethodReferenceTestKindsBase(blank)"); + assertEquals("MethodReferenceTestKindsBase(blank)", var.make().toString()); } + @Test public void testMRTopLevelArg() { SN1 var = MethodReferenceTestKindsBase::new; - assertEquals(var.make("name").toString(), "MethodReferenceTestKindsBase(name)"); + assertEquals("MethodReferenceTestKindsBase(name)", var.make("name").toString()); } + @Test public void testMRImplicitInner() { SN0 var = MethodReferenceTestKinds.In::new; - assertEquals(var.make().toString(), "In(blank)"); + assertEquals("In(blank)", var.make().toString()); } + @Test public void testMRImplicitInnerArg() { SN1 var = MethodReferenceTestKinds.In::new; - assertEquals(var.make("name").toString(), "In(name)"); + assertEquals("In(name)", var.make("name").toString()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java index 05a9f9f3997..003ca2b4781 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,16 +25,15 @@ * @test * @bug 8028739 * @summary javac generates incorrect descriptor for MethodHandle::invoke - * @run testng MethodReferenceTestMethodHandle + * @run junit MethodReferenceTestMethodHandle */ import java.lang.invoke.*; import java.util.*; -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class MethodReferenceTestMethodHandle { MethodHandles.Lookup lookup = MethodHandles.lookup(); @@ -51,6 +50,7 @@ public class MethodReferenceTestMethodHandle { void apply(List st, int idx, Object v) throws Throwable; } + @Test public void testVirtual() throws Throwable { MethodType mt = MethodType.methodType(String.class, char.class, char.class); @@ -69,6 +69,7 @@ public class MethodReferenceTestMethodHandle { assertEquals("oome otring to oearch", ((ReplaceItf) ms::invoke).apply("some string to search", 's', 'o')); } + @Test public void testStatic() throws Throwable { MethodType fmt = MethodType.methodType(String.class, String.class, (new Object[1]).getClass()); MethodHandle fms = lookup.findStatic(String.class, "format", fmt); @@ -83,6 +84,7 @@ public class MethodReferenceTestMethodHandle { assertEquals("Testing One 2 3 four", ff2.apply("Testing %s %d %x %s", "One", new Integer(2), 3, "four")); } + @Test public void testVoid() throws Throwable { MethodType pmt = MethodType.methodType(void.class, int.class, Object.class); MethodHandle pms = lookup.findVirtual(List.class, "add", pmt); diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java index 15a35a7d0b3..26cf3ffd61d 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestNew + * @run junit MethodReferenceTestNew */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestNew { interface M0 { @@ -105,32 +103,36 @@ public class MethodReferenceTestNew { } } + @Test public void testConstructorReference0() { M0 q; q = N0::new; - assertEquals(q.m().getClass().getSimpleName(), "N0"); + assertEquals("N0", q.m().getClass().getSimpleName()); } + @Test public void testConstructorReference1() { M1 q; q = N1::new; - assertEquals(q.m(14).getClass().getSimpleName(), "N1"); + assertEquals("N1", q.m(14).getClass().getSimpleName()); } + @Test public void testConstructorReference2() { M2 q; q = N2::new; - assertEquals(q.m(7, "hi").toString(), "N2(7,hi)"); + assertEquals("N2(7,hi)", q.m(7, "hi").toString()); } + @Test public void testConstructorReferenceVarArgs() { MV q; q = NV::new; - assertEquals(q.m(5, 45).toString(), "NV(50)"); + assertEquals("NV(50)", q.m(5, 45).toString()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java index 1f61de4da8f..324428653da 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestNewInner + * @run junit MethodReferenceTestNewInner */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestNewInner { String note = "NO NOTE"; @@ -120,26 +118,29 @@ public class MethodReferenceTestNewInner { assertEquals(q.m(new MethodReferenceTestNewInner()).getClass().getSimpleName(), "N0"); } */ + @Test public void testConstructorReference0() { M0 q; q = N0::new; - assertEquals(q.m().getClass().getSimpleName(), "N0"); + assertEquals("N0", q.m().getClass().getSimpleName()); } + @Test public void testConstructorReference1() { M1 q; q = N1::new; - assertEquals(q.m(14).getClass().getSimpleName(), "N1"); + assertEquals("N1", q.m(14).getClass().getSimpleName()); } + @Test public void testConstructorReference2() { M2 q; note = "T2"; q = N2::new; - assertEquals(q.m(7, "hi").toString(), "T2:N2(7,hi)"); + assertEquals("T2:N2(7,hi)", q.m(7, "hi").toString()); } /*** diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java index ae3d7d6cc83..a172dbd8ce0 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8011591 * @summary BootstrapMethodError when capturing constructor ref to local classes - * @run testng MethodReferenceTestNewInnerImplicitArgs + * @run junit MethodReferenceTestNewInnerImplicitArgs */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * Test the case that a constructor has implicit parameters added to @@ -39,7 +38,6 @@ import static org.testng.Assert.assertEquals; * @author Robert Field */ -@Test public class MethodReferenceTestNewInnerImplicitArgs { @@ -56,7 +54,8 @@ public class MethodReferenceTestNewInnerImplicitArgs { S m(int i, int j); } - public static void testConstructorReferenceImplicitParameters() { + @Test + public void testConstructorReferenceImplicitParameters() { String title = "Hey"; String a2 = "!!!"; class MS extends S { @@ -66,7 +65,7 @@ public class MethodReferenceTestNewInnerImplicitArgs { } I result = MS::new; - assertEquals(result.m().b, "Hey!!!"); + assertEquals("Hey!!!", result.m().b); class MS2 extends S { MS2(int x, int y) { @@ -75,6 +74,6 @@ public class MethodReferenceTestNewInnerImplicitArgs { } I2 result2 = MS2::new; - assertEquals(result2.m(8, 4).b, "Hey8!!!4"); + assertEquals("Hey8!!!4", result2.m(8, 4).b); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java index 02fca01ee4a..10aeed014df 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase1 + * @run junit MethodReferenceTestSueCase1 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase1 { public interface Sam2 { public String get(T target, String s); } @@ -46,7 +44,8 @@ public class MethodReferenceTestSueCase1 { String m() { return var.get(new MethodReferenceTestSueCase1(), ""); } + @Test public void testSueCase1() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java index 4d558484337..f69fb1e4778 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase2 + * @run junit MethodReferenceTestSueCase2 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase2 { public interface Sam2 { public String get(T target, String s); } @@ -46,7 +44,8 @@ public class MethodReferenceTestSueCase2 { String m() { return var.get(new MethodReferenceTestSueCase2(), ""); } + @Test public void testSueCase2() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java index 9824163d5a3..abe86b37aad 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase4 + * @run junit MethodReferenceTestSueCase4 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase4 { public interface Sam2 { public String get(T target, String s); } @@ -51,7 +49,8 @@ public class MethodReferenceTestSueCase4 { String instanceMethod(String s) { return "2"; } } + @Test public void testSueCase4() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java index 1c1afcbd70d..29bf0e56469 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSuper + * @run junit MethodReferenceTestSuper */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -77,7 +76,6 @@ class SPRB extends SPRA { } -@Test public class MethodReferenceTestSuper extends SPRB { String xsA_M(String s) { @@ -93,26 +91,27 @@ public class MethodReferenceTestSuper extends SPRB { return "_BMxsM:" + s; } + @Test public void testMethodReferenceSuper() { SPRI q; q = super::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = super::xsA_M; - assertEquals(q.m("*"), "A_MxsA:*"); + assertEquals("A_MxsA:*", q.m("*")); q = super::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = super::xsABM; - assertEquals(q.m("*"), "ABMxsB:*"); + assertEquals("ABMxsB:*", q.m("*")); q = super::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); q = super::xs_BM; - assertEquals(q.m("*"), "_BMxsB:*"); + assertEquals("_BMxsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java index 316f876e5f4..3db1119f8c5 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSuperDefault + * @run junit MethodReferenceTestSuperDefault */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -61,20 +60,20 @@ interface DSPRB extends DSPRA { } -@Test public class MethodReferenceTestSuperDefault implements DSPRB { + @Test public void testMethodReferenceSuper() { DSPRI q; q = DSPRB.super::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = DSPRB.super::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = DSPRB.super::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java index 46962975582..f0f66756a3f 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestTypeConversion + * @run junit MethodReferenceTestTypeConversion */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -40,21 +39,22 @@ class MethodReferenceTestTypeConversion_E { T xI(T t) { return t; } } -@Test public class MethodReferenceTestTypeConversion { interface ISi { int m(Short a); } interface ICc { char m(Character a); } + @Test public void testUnboxObjectToNumberWiden() { ISi q = (new MethodReferenceTestTypeConversion_E())::xI; - assertEquals(q.m((short)77), (short)77); + assertEquals((short)77, q.m((short)77)); } + @Test public void testUnboxObjectToChar() { ICc q = (new MethodReferenceTestTypeConversion_E())::xI; - assertEquals(q.m('@'), '@'); + assertEquals('@', q.m('@')); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java index c430af2ef24..ef0af7ac1af 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,19 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgs + * @run junit MethodReferenceTestVarArgs */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestVarArgs { interface SII { @@ -127,75 +126,81 @@ public class MethodReferenceTestVarArgs { return sb.toString(); } + @Test public void testVarArgsSuperclass() { SII q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsArray() { Sai q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsII() { SII q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsiii() { Siii q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsi() { Si q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsaO() { SaO q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java index 989ed97aa6a..1a84660f592 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsExt + * @run junit MethodReferenceTestVarArgsExt */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -49,80 +49,85 @@ interface NXai { String m(int[] a); } interface NXvi { String m(int... va); } -@Test public class MethodReferenceTestVarArgsExt { // These should be processed as var args + @Test public void testVarArgsNXSuperclass() { NXII q; q = (new Ext())::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsNXArray() { NXai q; q = (new Ext())::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsNXII() { NXII q; q = (new Ext())::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = (new Ext())::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = (new Ext())::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = (new Ext())::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsNXiii() { NXiii q; q = (new Ext())::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = (new Ext())::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = (new Ext())::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = (new Ext())::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsNXi() { NXi q; q = (new Ext())::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = (new Ext())::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = (new Ext())::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = (new Ext())::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsNXaO() { NXaO q; q = (new Ext())::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java index 575cb2b2a43..7eebb7c0772 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsSuper + * @run junit MethodReferenceTestVarArgsSuper */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -95,7 +95,6 @@ class MethodReferenceTestVarArgsSuper_Sub { } } -@Test public class MethodReferenceTestVarArgsSuper extends MethodReferenceTestVarArgsSuper_Sub { interface SPRII { String m(Integer a, Integer b); } @@ -132,74 +131,80 @@ public class MethodReferenceTestVarArgsSuper extends MethodReferenceTestVarArgsS // These should be processed as var args + @Test public void testVarArgsSPRSuperclass() { SPRII q; q = super::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsSPRArray() { SPRai q; q = super::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsSPRII() { SPRII q; q = super::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = super::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = super::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = super::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsSPRiii() { SPRiii q; q = super::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = super::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = super::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = super::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsSPRi() { SPRi q; q = super::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = super::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = super::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = super::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsSPRaO() { SPRaO q; q = super::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java index 81ea6cbee66..e3b8ce395cb 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsSuperDefault + * @run junit MethodReferenceTestVarArgsSuperDefault */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -95,7 +95,6 @@ interface MethodReferenceTestVarArgsSuperDefault_I { } } -@Test public class MethodReferenceTestVarArgsSuperDefault implements MethodReferenceTestVarArgsSuperDefault_I { interface DSPRII { String m(Integer a, Integer b); } @@ -112,75 +111,81 @@ public class MethodReferenceTestVarArgsSuperDefault implements MethodReferenceTe // These should be processed as var args + @Test public void testVarArgsSPRSuperclass() { DSPRII q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsSPRArray() { DSPRai q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsSPRII() { DSPRII q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsSPRiii() { DSPRiii q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsSPRi() { DSPRi q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsSPRaO() { DSPRaO q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java index 5066758ee10..6369cf387b6 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsThis + * @run junit MethodReferenceTestVarArgsThis */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -49,7 +49,6 @@ interface Nsai { String m(int[] a); } interface Nsvi { String m(int... va); } -@Test public class MethodReferenceTestVarArgsThis { // These should be processed as var args @@ -109,75 +108,81 @@ public class MethodReferenceTestVarArgsThis { return sb.toString(); } + @Test public void testVarArgsNsSuperclass() { NsII q; q = this::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsNsArray() { Nsai q; q = this::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsNsII() { NsII q; q = this::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = this::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = this::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = this::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsNsiii() { Nsiii q; q = this::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = this::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = this::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = this::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsNsi() { Nsi q; q = this::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = this::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = this::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = this::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsNsaO() { NsaO q; q = this::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java index aaf9df950d4..fa835995e0a 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,16 +24,15 @@ /** * @test * @summary test for VarHandle signature polymorphic methods - * @run testng MethodReferenceTestVarHandle + * @run junit MethodReferenceTestVarHandle */ import java.lang.invoke.*; import java.util.*; -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class MethodReferenceTestVarHandle { interface Setter { @@ -44,6 +43,7 @@ public class MethodReferenceTestVarHandle { int apply(int[] arr, int idx); } + @Test public void testSet() throws Throwable { VarHandle vh = MethodHandles.arrayElementVarHandle(int[].class); @@ -54,6 +54,7 @@ public class MethodReferenceTestVarHandle { assertEquals(42, data[0]); } + @Test public void testGet() throws Throwable { VarHandle vh = MethodHandles.arrayElementVarHandle(int[].class); diff --git a/test/langtools/tools/javac/lambdaShapes/TEST.properties b/test/langtools/tools/javac/lambdaShapes/TEST.properties index 8a7db8dbee7..07bc537f116 100644 --- a/test/langtools/tools/javac/lambdaShapes/TEST.properties +++ b/test/langtools/tools/javac/lambdaShapes/TEST.properties @@ -1,4 +1,4 @@ -TestNG.dirs = tools/javac/lambdaShapes +JUnit.dirs = tools/javac/lambdaShapes modules = \ jdk.compiler/com.sun.tools.javac.util diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java index 813c588349f..2ecf714092f 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -42,12 +42,15 @@ import javax.tools.SimpleJavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; -import org.testng.annotations.AfterSuite; -import org.testng.annotations.Test; -import org.testng.annotations.BeforeSuite; -import org.testng.annotations.DataProvider; -import static org.testng.Assert.*; +import org.junit.jupiter.api.AfterAll; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; +@ParameterizedClass +@MethodSource("caseGenerator") public class FDTest { public enum TestKind { @@ -63,7 +66,7 @@ public class FDTest { public static JavaCompiler comp; public static StandardJavaFileManager fm; - @BeforeSuite + @BeforeAll static void init() { // create default shared JavaCompiler - reused across multiple // compilations @@ -72,7 +75,7 @@ public class FDTest { fm = comp.getStandardFileManager(null, null, null); } - @AfterSuite + @AfterAll static void teardown() throws IOException { fm.close(); } @@ -87,14 +90,13 @@ public class FDTest { teardown(); } - @Test(dataProvider = "fdCases") - public void testOneCase(TestKind tk, Hierarchy hs) + @Test + public void testOneCase() throws Exception { FDTest.runTest(tk, hs, comp, fm); } - @DataProvider(name = "fdCases") - public Object[][] caseGenerator() { + public static Object[][] caseGenerator() { List> cases = generateCases(); Object[][] fdCases = new Object[cases.size()][]; for (int i = 0; i < cases.size(); ++i) { @@ -127,8 +129,6 @@ public class FDTest { DefenderTestSource source; DiagnosticChecker diagChecker; - public FDTest() {} - FDTest(TestKind tk, Hierarchy hs) { this.tk = tk; this.hs = hs; diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java index a1a37559e6d..dcfff28949e 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,17 +23,16 @@ package org.openjdk.tests.separate; -import org.testng.ITestResult; -import org.testng.annotations.AfterMethod; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import org.junit.jupiter.api.AfterEach; import static org.openjdk.tests.separate.SourceModel.Class; import static org.openjdk.tests.separate.SourceModel.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class TestHarness { @@ -69,7 +68,7 @@ public class TestHarness { verboseLocal.set(Boolean.TRUE); } - @AfterMethod + @AfterEach public void reset() { if (!this.verbose) { verboseLocal.set(Boolean.FALSE); @@ -87,16 +86,6 @@ public class TestHarness { return flags.toArray(new Compiler.Flags[0]); } - @AfterMethod - public void printError(ITestResult result) { - if (result.getStatus() == ITestResult.FAILURE) { - String clsName = result.getTestClass().getName(); - clsName = clsName.substring(clsName.lastIndexOf(".") + 1); - System.out.println("Test " + clsName + "." + - result.getName() + " FAILED"); - } - } - private static final ConcreteMethod stdCM = ConcreteMethod.std("-1"); private static final AbstractMethod stdAM = new AbstractMethod("int", stdMethodName); @@ -193,7 +182,7 @@ public class TestHarness { Object res = m.invoke(null); assertNotNull(res); if (value != null) { - assertEquals(res, value); + assertEquals(value, res); } } catch (InvocationTargetException | IllegalAccessException e) { fail("Unexpected exception thrown: " + e.getCause(), e.getCause()); diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java index 93629947f3a..b876a2538e0 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ package org.openjdk.tests.vm; import org.openjdk.tests.separate.Compiler; import org.openjdk.tests.separate.TestHarness; -import org.testng.annotations.Test; import static org.openjdk.tests.separate.SourceModel.AbstractMethod; import static org.openjdk.tests.separate.SourceModel.AccessFlag; @@ -36,11 +35,11 @@ import static org.openjdk.tests.separate.SourceModel.Extends; import static org.openjdk.tests.separate.SourceModel.Interface; import static org.openjdk.tests.separate.SourceModel.MethodParameter; import static org.openjdk.tests.separate.SourceModel.TypeParameter; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; -@Test(groups = "vm") public class DefaultMethodsTest extends TestHarness { public DefaultMethodsTest() { super(false, false); @@ -51,6 +50,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() == 22 */ + @Test public void testHarnessInvokeVirtual() { Class C = new Class("C", ConcreteMethod.std("22")); assertInvokeVirtualEquals(22, C); @@ -62,6 +62,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: I i = new C(); i.m() == 33; */ + @Test public void testHarnessInvokeInterface() { Interface I = new Interface("I", AbstractMethod.std()); Class C = new Class("C", I, ConcreteMethod.std("33")); @@ -73,6 +74,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws NoSuchMethod */ + @Test public void testHarnessThrows() { Class C = new Class("C"); assertThrows(NoSuchMethodError.class, C); @@ -85,6 +87,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: I i = new C(); i.m() == 44; */ + @Test public void testBasicDefault() { Interface I = new Interface("I", DefaultMethod.std("44")); Class C = new Class("C", I); @@ -102,6 +105,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: I i = new C(); i.m() == 44; */ + @Test public void testFarDefault() { Interface I = new Interface("I", DefaultMethod.std("44")); Interface J = new Interface("J", I); @@ -121,6 +125,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: K k = new C(); k.m() == 44; */ + @Test public void testOverrideAbstract() { Interface I = new Interface("I", AbstractMethod.std()); Interface J = new Interface("J", I, DefaultMethod.std("44")); @@ -138,6 +143,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 55; * TEST: I i = new C(); i.m() == 55; */ + @Test public void testExisting() { Interface I = new Interface("I", DefaultMethod.std("44")); Class C = new Class("C", I, ConcreteMethod.std("55")); @@ -154,6 +160,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testInherited() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -171,6 +178,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: D d = new D(); d.m() == 11; * TEST: I i = new D(); i.m() == 11; */ + @Test public void testExistingInherited() { Interface I = new Interface("I", DefaultMethod.std("99")); Class C = new Class("C", ConcreteMethod.std("11")); @@ -188,6 +196,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: D d = new D(); d.m() == 22; * TEST: I i = new D(); i.m() == 22; */ + @Test public void testExistingInheritedOverride() { Interface I = new Interface("I", DefaultMethod.std("99")); Class C = new Class("C", I, ConcreteMethod.std("11")); @@ -207,6 +216,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: E e = new E(); e.m() == 22; * TEST: J j = new E(); j.m() == 22; */ + @Test public void testExistingInheritedPlusDefault() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -226,6 +236,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 77; * TEST: I i = new C(); i.m() == 77; */ + @Test public void testInheritedWithConcrete() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -243,6 +254,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 66; * TEST: I i = new C(); i.m() == 66; */ + @Test public void testInheritedWithConcreteAndImpl() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -259,6 +271,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws ICCE */ + @Test public void testConflict() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -274,6 +287,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() == 88 */ + @Test public void testAmbiguousReabstract() { Interface I = new Interface("I", AbstractMethod.std()); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -293,6 +307,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: K k = new C(); k.m() == 99 * TEST: I i = new C(); i.m() == 99 */ + @Test public void testDiamond() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I); @@ -320,6 +335,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: L l = new C(); l.m() == 99 * TEST: M m = new C(); m.m() == 99 */ + @Test public void testExpandedDiamond() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I); @@ -343,6 +359,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws AME */ + @Test public void testReabstract() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I, AbstractMethod.std()); @@ -360,6 +377,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testShadow() { Interface I = new Interface("I", DefaultMethod.std("88")); Interface J = new Interface("J", I, DefaultMethod.std("99")); @@ -379,6 +397,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testDisqualified() { Interface I = new Interface("I", DefaultMethod.std("88")); Interface J = new Interface("J", I, DefaultMethod.std("99")); @@ -396,6 +415,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m("string") == 88; * TEST: I i = new C(); i.m("string") == 88; */ + @Test public void testSelfFill() { // This test ensures that a concrete method overrides a default method // that matches at the language-level, but has a different method @@ -426,6 +446,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C.class.getMethod("m").invoke(new C()) == 99 */ + @Test public void testReflectCall() { Interface I = new Interface("I", DefaultMethod.std("99")); //workaround accessibility issue when loading C with DirectedClassLoader @@ -468,7 +489,7 @@ public class DefaultMethodsTest extends TestHarness { } assertNotNull(res); - assertEquals(res.intValue(), 99); + assertEquals(99, res.intValue()); compiler.cleanup(); } @@ -485,6 +506,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m("A","B","C") == 88; * TEST: K k = new C(); k.m("A","B","C") == 88; */ + @Test public void testBridges() { DefaultMethod dm = new DefaultMethod("int", stdMethodName, "return 99;", new MethodParameter("T", "t"), new MethodParameter("V", "v"), @@ -538,6 +560,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 88; * TEST: I i = new C(); i.m() == 88; */ + @Test public void testSuperBasic() { Interface J = new Interface("J", DefaultMethod.std("88")); Interface I = new Interface("I", J, new DefaultMethod( @@ -559,6 +582,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() throws ICCE * TODO: add case for K k = new C(); k.m() throws ICCE */ + @Test public void testSuperConflict() { Interface K = new Interface("K", DefaultMethod.std("99")); Interface L = new Interface("L", DefaultMethod.std("101")); @@ -581,6 +605,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 99 * TODO: add case for J j = new C(); j.m() == ??? */ + @Test public void testSuperDisqual() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I, DefaultMethod.std("55")); @@ -600,6 +625,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() throws AME * TODO: add case for I i = new C(); i.m() throws AME */ + @Test public void testSuperNull() { Interface J = new Interface("J", AbstractMethod.std()); Interface I = new Interface("I", J, new DefaultMethod( @@ -621,6 +647,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: I i = new C(); i.m("") == 88; */ + @Test public void testSuperGeneric() { Interface J = new Interface("J", new TypeParameter("T"), new DefaultMethod("int", stdMethodName, "return 88;", @@ -646,6 +673,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m("string") == 44 */ + @Test public void testSuperGenericDisqual() { MethodParameter t = new MethodParameter("T", "t"); MethodParameter s = new MethodParameter("String", "s"); @@ -672,6 +700,7 @@ public class DefaultMethodsTest extends TestHarness { * class S { Object foo() { return (new D()).m(); } // link sig: ()LInteger; * TEST: S s = new S(); s.foo() == new Integer(99) */ + @Test public void testCovarBridge() { Interface I = new Interface("I", new DefaultMethod( "Integer", "m", "return new Integer(88);")); @@ -700,6 +729,7 @@ public class DefaultMethodsTest extends TestHarness { * class S { Object foo() { return (new D()).m(); } // link sig: ()LInteger; * TEST: S s = new S(); s.foo() == new Integer(88) */ + @Test public void testNoCovarNoBridge() { Interface I = new Interface("I", new DefaultMethod( "Integer", "m", "return new Integer(88);")); @@ -732,6 +762,7 @@ public class DefaultMethodsTest extends TestHarness { * It verifies that default method analysis occurs when mirandas have been * inherited and the supertypes don't have any overpass methods. */ + @Test public void testNoNewMiranda() { Interface J = new Interface("J", AbstractMethod.std()); Interface I = new Interface("I", J, DefaultMethod.std("99")); @@ -755,6 +786,7 @@ public class DefaultMethodsTest extends TestHarness { * Test that a erased-signature-matching method does not implement * non-language-level matching methods */ + @Test public void testNonConcreteFill() { AbstractMethod ipm = new AbstractMethod("int", "m", new MethodParameter("T", "t"), @@ -809,6 +841,7 @@ public class DefaultMethodsTest extends TestHarness { assertInvokeInterfaceEquals(99, C, I.with("String", "String", "String"), ipm, a, a, a); } + @Test public void testStrictfpDefault() { try { java.lang.Class.forName("org.openjdk.tests.vm.StrictfpDefault"); diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java index 4fef2df615d..904883a0c3e 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,11 +28,8 @@ package org.openjdk.tests.vm; import java.util.*; -import org.testng.ITestResult; -import org.testng.annotations.Test; -import org.testng.annotations.DataProvider; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.AfterSuite; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.openjdk.tests.separate.*; import org.openjdk.tests.separate.Compiler; @@ -41,7 +38,10 @@ import org.openjdk.tests.shapegen.Hierarchy; import org.openjdk.tests.shapegen.HierarchyGenerator; import org.openjdk.tests.shapegen.ClassCase; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.openjdk.tests.separate.SourceModel.*; import static org.openjdk.tests.separate.SourceModel.Class; import static org.openjdk.tests.separate.SourceModel.Method; @@ -55,7 +55,6 @@ public class FDSeparateCompilationTest extends TestHarness { super(false, true); } - @DataProvider(name = "allShapes", parallel = true) public Object[][] hierarchyGenerator() { ArrayList allCases = new ArrayList<>(); @@ -92,7 +91,9 @@ public class FDSeparateCompilationTest extends TestHarness { private static final ConcreteMethod canonicalMethod = new ConcreteMethod( "String", "m", "returns " + EMPTY + ";", AccessFlag.PUBLIC); - @Test(enabled = false, groups = "vm", dataProvider = "allShapes") + @Disabled + @ParameterizedTest + @MethodSource("hierarchyGenerator") public void separateCompilationTest(Hierarchy hs) { ClassCase cc = hs.root; Type type = sourceTypeFrom(hs.root); @@ -118,17 +119,8 @@ public class FDSeparateCompilationTest extends TestHarness { } } - @AfterMethod - public void printCaseError(ITestResult result) { - if (result.getStatus() == ITestResult.FAILURE) { - Hierarchy hs = (Hierarchy)result.getParameters()[0]; - System.out.println("Separate compilation case " + hs); - printCaseDetails(hs); - } - } - - @AfterSuite - public void cleanupCompilerCache() { + @AfterAll + public static void cleanupCompilerCache() { Compiler.purgeCache(); } diff --git a/test/langtools/tools/javac/multicatch/Neg01.java b/test/langtools/tools/javac/multicatch/Neg01.java index 378e4f285cb..86a2ae952a1 100644 --- a/test/langtools/tools/javac/multicatch/Neg01.java +++ b/test/langtools/tools/javac/multicatch/Neg01.java @@ -3,7 +3,6 @@ * @bug 6943289 * * @summary Project Coin: Improved Exception Handling for Java (aka 'multicatch') - * @author darcy * @compile/fail/ref=Neg01.out -XDrawDiagnostics Neg01.java */ diff --git a/test/langtools/tools/javac/multicatch/Neg01.out b/test/langtools/tools/javac/multicatch/Neg01.out index 56eb1958a52..f0ddda9bce6 100644 --- a/test/langtools/tools/javac/multicatch/Neg01.out +++ b/test/langtools/tools/javac/multicatch/Neg01.out @@ -1,2 +1,2 @@ -Neg01.java:22:19: compiler.err.except.never.thrown.in.try: Neg01.B2 +Neg01.java:21:19: compiler.err.except.never.thrown.in.try: Neg01.B2 1 error diff --git a/test/langtools/tools/javac/multicatch/Neg01eff_final.java b/test/langtools/tools/javac/multicatch/Neg01eff_final.java index e2c78f5e055..933d7d1599d 100644 --- a/test/langtools/tools/javac/multicatch/Neg01eff_final.java +++ b/test/langtools/tools/javac/multicatch/Neg01eff_final.java @@ -3,7 +3,6 @@ * @bug 6943289 * * @summary Project Coin: Improved Exception Handling for Java (aka 'multicatch') - * @author darcy * @compile/fail/ref=Neg01eff_final.out -XDrawDiagnostics Neg01eff_final.java */ diff --git a/test/langtools/tools/javac/multicatch/Neg01eff_final.out b/test/langtools/tools/javac/multicatch/Neg01eff_final.out index 4327ef45b9b..b110d4468a1 100644 --- a/test/langtools/tools/javac/multicatch/Neg01eff_final.out +++ b/test/langtools/tools/javac/multicatch/Neg01eff_final.out @@ -1,2 +1,2 @@ -Neg01eff_final.java:22:19: compiler.err.except.never.thrown.in.try: Neg01eff_final.B2 +Neg01eff_final.java:21:19: compiler.err.except.never.thrown.in.try: Neg01eff_final.B2 1 error diff --git a/test/langtools/tools/javac/multicatch/Neg07.java b/test/langtools/tools/javac/multicatch/Neg07.java index e5869e6a2cc..a2c1704c6b2 100644 --- a/test/langtools/tools/javac/multicatch/Neg07.java +++ b/test/langtools/tools/javac/multicatch/Neg07.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 7039822 * @summary Verify typing of lub of exception parameter w.r.t getClass - * @author Joseph D. Darcy * @compile/fail/ref=Neg07.out -XDrawDiagnostics Neg07.java */ diff --git a/test/langtools/tools/javac/multicatch/Neg07.out b/test/langtools/tools/javac/multicatch/Neg07.out index 32030e17695..0acdba265da 100644 --- a/test/langtools/tools/javac/multicatch/Neg07.out +++ b/test/langtools/tools/javac/multicatch/Neg07.out @@ -1,2 +1,2 @@ -Neg07.java:14:56: compiler.err.prob.found.req: (compiler.misc.inconvertible.types: java.lang.Class, java.lang.Class) +Neg07.java:13:56: compiler.err.prob.found.req: (compiler.misc.inconvertible.types: java.lang.Class, java.lang.Class) 1 error diff --git a/test/langtools/tools/javac/multicatch/Pos10.java b/test/langtools/tools/javac/multicatch/Pos10.java index c66fa5334cd..d108c8f9671 100644 --- a/test/langtools/tools/javac/multicatch/Pos10.java +++ b/test/langtools/tools/javac/multicatch/Pos10.java @@ -25,7 +25,6 @@ * @test * @bug 7039822 * @summary Verify lub of an exception parameter can be an intersection type - * @author Joseph D. Darcy */ public class Pos10 { diff --git a/test/langtools/tools/javac/platform/CompilationTest.java b/test/langtools/tools/javac/platform/CompilationTest.java new file mode 100644 index 00000000000..93fb2fb2678 --- /dev/null +++ b/test/langtools/tools/javac/platform/CompilationTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8365060 + * @summary Verify javac can compile given snippets with --release + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.platform + * jdk.compiler/com.sun.tools.javac.util:+open + * @build toolbox.ToolBox CompilationTest + * @run main CompilationTest + */ + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import toolbox.JavacTask; +import toolbox.Task; +import toolbox.ToolBox; + +public class CompilationTest { + + private final ToolBox tb = new ToolBox(); + public static void main(String... args) throws IOException, URISyntaxException { + CompilationTest t = new CompilationTest(); + + t.testJdkNet(); + } + + void testJdkNet() throws IOException { + Path root = Paths.get("."); + Path classes = root.resolve("classes"); + Files.createDirectories(classes); + + new JavacTask(tb) + .outdir(classes) + .options("--release", "8", + "-XDrawDiagnostics") + .sources(""" + import jdk.net.ExtendedSocketOptions; + public class Test { + } + """) + .run() + .writeAll() + .getOutputLines(Task.OutputKind.DIRECT); + } + +} diff --git a/test/langtools/tools/javac/processing/6365040/T6365040.java b/test/langtools/tools/javac/processing/6365040/T6365040.java index 4413ff1375c..c538609800d 100644 --- a/test/langtools/tools/javac/processing/6365040/T6365040.java +++ b/test/langtools/tools/javac/processing/6365040/T6365040.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 6365040 6358129 * @summary Test -processor foo,bar,baz - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler @@ -13,7 +12,7 @@ * @compile -processor ProcFoo,ProcBar,T6365040 -proc:only T6365040.java * @compile -processor T6365040 -proc:only T6365040.java * @compile -processor T6365040,NotThere, -proc:only T6365040.java - * @compile/fail/ref=T6365040.out -XDrawDiagnostics -processor NotThere -proc:only T6365040.java + * @compile/fail/ref=T6365040.out -XDrawDiagnostics -processor NotThere -proc:only T6365040.java * @compile/fail/ref=T6365040.out -XDrawDiagnostics -processor NotThere,T6365040 -proc:only T6365040.java */ @@ -23,7 +22,6 @@ import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.element.TypeElement; - public class T6365040 extends JavacTestingAbstractProcessor { public boolean process(Set annotations, RoundEnvironment roundEnvironment) { diff --git a/test/langtools/tools/javac/processing/6378728/T6378728.java b/test/langtools/tools/javac/processing/6378728/T6378728.java index 9ee62051027..791296fb754 100644 --- a/test/langtools/tools/javac/processing/6378728/T6378728.java +++ b/test/langtools/tools/javac/processing/6378728/T6378728.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,6 @@ /* @test * @bug 6378728 * @summary Verify -proc:only doesn't produce class files - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler * @compile T6378728.java diff --git a/test/langtools/tools/javac/processing/6634138/T6634138.java b/test/langtools/tools/javac/processing/6634138/T6634138.java index 872192d51ec..baf9bff3b63 100644 --- a/test/langtools/tools/javac/processing/6634138/T6634138.java +++ b/test/langtools/tools/javac/processing/6634138/T6634138.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,6 @@ /* * @test * @bug 6634138 - * @author Joseph D. Darcy * @summary Verify source files output after processing is over are compiled * @library /tools/javac/lib * @modules java.compiler diff --git a/test/langtools/tools/javac/processing/completion/TestCompletions.java b/test/langtools/tools/javac/processing/completion/TestCompletions.java index 87d947f173e..c181b0fc84d 100644 --- a/test/langtools/tools/javac/processing/completion/TestCompletions.java +++ b/test/langtools/tools/javac/processing/completion/TestCompletions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6341177 * @summary Some simple tests of the methods in Completions - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/environment/TestSourceVersion.java b/test/langtools/tools/javac/processing/environment/TestSourceVersion.java index e31664b8a93..473b14ad39c 100644 --- a/test/langtools/tools/javac/processing/environment/TestSourceVersion.java +++ b/test/langtools/tools/javac/processing/environment/TestSourceVersion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6402506 8028545 8028543 * @summary Test that getSourceVersion works properly - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java b/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java index 33b19543ef7..ddcaaf7b60d 100644 --- a/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java +++ b/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6397298 6400986 6425592 6449798 6453386 6508401 6498938 6911854 8030049 8038080 8032230 8190886 * @summary Tests that getElementsAnnotatedWith[Any] methods work properly. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java b/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java index 300afaad524..3a7ee877c1f 100644 --- a/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java +++ b/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 6403459 * @summary Test that generating programs with syntax errors is a fatal condition - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java b/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java index 8c397b20f62..bd1dea81a40 100644 --- a/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java +++ b/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6406212 * @summary Test that annotation processor options with illegal syntax are rejected - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler/com.sun.tools.javac.main * @build JavacTestingAbstractProcessor CompileFail diff --git a/test/langtools/tools/javac/processing/errors/TestReturnCode.java b/test/langtools/tools/javac/processing/errors/TestReturnCode.java index 749044edd6f..5ca6a50e3c6 100644 --- a/test/langtools/tools/javac/processing/errors/TestReturnCode.java +++ b/test/langtools/tools/javac/processing/errors/TestReturnCode.java @@ -25,7 +25,6 @@ * @test * @bug 6403468 * @summary Test that an erroneous return code results from raising an error. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler/com.sun.tools.javac.main * @build JavacTestingAbstractProcessor CompileFail diff --git a/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java b/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java index e522afa9ce8..6eac5475c0b 100644 --- a/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java +++ b/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6380018 6453386 6457283 * @summary Test that the constraints guaranteed by the Filer and maintained - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/filer/TestGetResource.java b/test/langtools/tools/javac/processing/filer/TestGetResource.java index 073757d1217..cf8ac2aafeb 100644 --- a/test/langtools/tools/javac/processing/filer/TestGetResource.java +++ b/test/langtools/tools/javac/processing/filer/TestGetResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6380018 6449798 * @summary Test Filer.getResource - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/filer/TestPackageInfo.java b/test/langtools/tools/javac/processing/filer/TestPackageInfo.java index 1e0659c9b12..1b11c75acde 100644 --- a/test/langtools/tools/javac/processing/filer/TestPackageInfo.java +++ b/test/langtools/tools/javac/processing/filer/TestPackageInfo.java @@ -25,7 +25,6 @@ * @test * @bug 6380018 6392177 6993311 8193462 * @summary Test the ability to create and process package-info.java files - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/messager/MessagerBasics.java b/test/langtools/tools/javac/processing/messager/MessagerBasics.java index 35c3b64ca2d..896f7b579b5 100644 --- a/test/langtools/tools/javac/processing/messager/MessagerBasics.java +++ b/test/langtools/tools/javac/processing/messager/MessagerBasics.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 6341173 6341072 * @summary Test presence of Messager methods - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/TestExceptions.java b/test/langtools/tools/javac/processing/model/TestExceptions.java index 993e084e157..e9f5846a7ca 100644 --- a/test/langtools/tools/javac/processing/model/TestExceptions.java +++ b/test/langtools/tools/javac/processing/model/TestExceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6794071 * @summary Test that exceptions have a proper parent class - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/TestSourceVersion.java b/test/langtools/tools/javac/processing/model/TestSourceVersion.java index 3a56bf76007..d53368d3b6d 100644 --- a/test/langtools/tools/javac/processing/model/TestSourceVersion.java +++ b/test/langtools/tools/javac/processing/model/TestSourceVersion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 7025809 8028543 6415644 8028544 8029942 8187951 8193291 8196551 8233096 8275308 * @summary Test latest, latestSupported, underscore as keyword, etc. - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java b/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java index c7504da4f89..de575841d12 100644 --- a/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java +++ b/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6449781 6930508 * @summary Test that reported names of anonymous classes are non-null. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler * @build JavacTestingAbstractProcessor TestAnonSourceNames diff --git a/test/langtools/tools/javac/processing/model/element/TestElement.java b/test/langtools/tools/javac/processing/model/element/TestElement.java index 9c878319110..4bfe6280fd5 100644 --- a/test/langtools/tools/javac/processing/model/element/TestElement.java +++ b/test/langtools/tools/javac/processing/model/element/TestElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6453386 * @summary Test basic properties of javax.lang.element.Element - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java b/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java index 4a37be9d7d7..2296214555f 100644 --- a/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java +++ b/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 8005046 8011052 8025087 * @summary Test basic properties of javax.lang.element.ExecutableElement - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestNames.java b/test/langtools/tools/javac/processing/model/element/TestNames.java index adbbf87c8e4..7e9ed193afd 100644 --- a/test/langtools/tools/javac/processing/model/element/TestNames.java +++ b/test/langtools/tools/javac/processing/model/element/TestNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6380016 * @summary Test that the constraints guaranteed by the Filer and maintained - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestPackageElement.java b/test/langtools/tools/javac/processing/model/element/TestPackageElement.java index f3749f6c5ff..83d7515677b 100644 --- a/test/langtools/tools/javac/processing/model/element/TestPackageElement.java +++ b/test/langtools/tools/javac/processing/model/element/TestPackageElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6449798 6399404 8173776 8163989 * @summary Test basic workings of PackageElement - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java b/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java index a0f133779ab..9a51613358a 100644 --- a/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java +++ b/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6911256 6964740 6967842 6961571 7025809 * @summary Test that the resource variable kind is appropriately set - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler * @build JavacTestingAbstractProcessor TestResourceVariable diff --git a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java index 1e252c01cc0..849f8df7b71 100644 --- a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java +++ b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6593082 * @summary MirroredTypeException constructor should not accept null - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java index 38c2db7ca13..2487ac77910 100644 --- a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java +++ b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,7 +31,6 @@ * @build JavacTestingAbstractProcessor * @compile Plurality.java * @compile -processor Plurality -proc:only Plurality.java - * @author Joseph D. Darcy */ import java.lang.annotation.*; import java.math.BigDecimal; diff --git a/test/langtools/tools/javac/processing/model/type/TestTypeKind.java b/test/langtools/tools/javac/processing/model/type/TestTypeKind.java index fc1ed62e2a5..9bc2e7e7cb6 100644 --- a/test/langtools/tools/javac/processing/model/type/TestTypeKind.java +++ b/test/langtools/tools/javac/processing/model/type/TestTypeKind.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6347716 * @summary Test TypeKind.isPrimitive - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java b/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java index d90b1ad34bf..fe8e5262be4 100644 --- a/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java +++ b/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6392818 * @summary Tests Elements.isDeprecated(Element) - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java b/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java index a9827b2aacc..b0b15cf9226 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java +++ b/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6471577 6517779 * @summary Test Elements.getConstantExpression - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java b/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java index d82addd8b1f..96c84db5bb7 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java +++ b/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6453386 8216404 8230337 * @summary Test Elements.getPackageOf - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java b/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java index f8292afd815..ae048af45cc 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java +++ b/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 8007574 * @summary Test Elements.isFunctionalInterface - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java b/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java index 2d16a878625..6269d5c7334 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java +++ b/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6937417 * @summary Test -Xprint on enum type with no constants - * @author Joseph D. Darcy * @compile -Xprint VacuousEnum.java */ public enum VacuousEnum { diff --git a/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java b/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java index a9bf8ecf8e6..6e9539b7a9e 100644 --- a/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java +++ b/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6406164 * @summary Test that ElementFilter iterable methods behave properly. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java b/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java index 5359d26e53f..b0898a30ca9 100644 --- a/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java +++ b/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 8175335 * @summary Test Types methods on module and package TypeMirrors - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler * @build JavacTestingAbstractProcessor TestPseudoTypeHandling diff --git a/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java b/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java index 54fe613acf1..24abf89ee02 100644 --- a/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java +++ b/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,11 +21,11 @@ * questions. */ + /* * @test * @bug 6376083 6376084 6458819 7025784 7025786 7025789 * @summary Test that warnings about source versions are output as expected. - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler * @compile TestSourceVersionWarnings.java diff --git a/test/langtools/tools/javac/records/BigRecordsToStringTest.java b/test/langtools/tools/javac/records/BigRecordsToStringTest.java index 87e269a9532..1e42cf03ebc 100644 --- a/test/langtools/tools/javac/records/BigRecordsToStringTest.java +++ b/test/langtools/tools/javac/records/BigRecordsToStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ * @test * @bug 8261847 * @summary test the output of the toString method of records with a large number of components - * @run testng BigRecordsToStringTest + * @run junit BigRecordsToStringTest */ import java.lang.reflect.Constructor; @@ -36,10 +36,9 @@ import java.lang.reflect.Parameter; import java.util.List; import java.util.function.Supplier; -import org.testng.annotations.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -@Test public class BigRecordsToStringTest { record BigInt( int i1,int i2,int i3,int i4,int i5,int i6,int i7,int i8,int i9,int i10, @@ -164,6 +163,7 @@ public class BigRecordsToStringTest { "i111=111, i112=112, i113=113, i114=114, i115=115, i116=116, i117=117, i118=118, i119=119, i120=120, i121=121, i122=122, i123=123, " + "i124=124, i125=125, i126=126, i127=127]"; + @Test public void testToStringOutput() { assertTrue(bigInt.toString().equals(BIG_INT_TO_STRING_OUTPUT)); assertTrue(bigLong.toString().equals(BIG_LONG_TO_STRING_OUTPUT)); diff --git a/test/langtools/tools/javac/records/RecordMemberTests.java b/test/langtools/tools/javac/records/RecordMemberTests.java index 7d358c21953..f7291e70cb0 100644 --- a/test/langtools/tools/javac/records/RecordMemberTests.java +++ b/test/langtools/tools/javac/records/RecordMemberTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ * @test * @bug 8246774 * @summary test several assertions on record classes members - * @run testng RecordMemberTests + * @run junit RecordMemberTests */ import java.lang.reflect.Constructor; @@ -36,10 +36,9 @@ import java.lang.reflect.Parameter; import java.util.List; import java.util.function.Supplier; -import org.testng.annotations.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -@Test public class RecordMemberTests { public record R1(int i, int j) {} @@ -63,43 +62,47 @@ public class RecordMemberTests { R3 r3 = new R3(1, 2); R4 r4 = new R4(1, 2); + @Test public void testConstruction() { for (int i : new int[] { r1.i, r2.i, r3.i, r1.i(), r2.i(), r3.i() }) - assertEquals(i, 1); + assertEquals(1, i); for (int j : new int[] { r1.j, r2.j, r3.j, r1.j(), r2.j(), r3.j() }) - assertEquals(j, 2); + assertEquals(2, j); - assertEquals(r4.i, 0); - assertEquals(r4.j, 0); + assertEquals(0, r4.i); + assertEquals(0, r4.j); } + @Test public void testConstructorParameterNames() throws ReflectiveOperationException { for (Class cl : List.of(R1.class, R2.class, R3.class, R4.class)) { Constructor c = cl.getConstructor(int.class, int.class); assertNotNull(c); Parameter[] parameters = c.getParameters(); - assertEquals(parameters.length, 2); - assertEquals(parameters[0].getName(), "i"); - assertEquals(parameters[1].getName(), "j"); + assertEquals(2, parameters.length); + assertEquals("i", parameters[0].getName()); + assertEquals("j", parameters[1].getName()); } } + @Test public void testSuperclass() { - assertEquals(R1.class.getSuperclass(), Record.class); + assertEquals(Record.class, R1.class.getSuperclass()); // class is final assertTrue((R1.class.getModifiers() & Modifier.FINAL) != 0); } + @Test public void testMandatedMembersPresent() throws ReflectiveOperationException { // fields are present, of the right type, final and private - assertEquals(R1.class.getDeclaredFields().length, 2); + assertEquals(2, R1.class.getDeclaredFields().length); for (String s : List.of("i", "j")) { Field iField = R1.class.getDeclaredField(s); - assertEquals(iField.getType(), int.class); - assertEquals((iField.getModifiers() & Modifier.STATIC), 0); + assertEquals(int.class, iField.getType()); + assertEquals(0, (iField.getModifiers() & Modifier.STATIC)); assertTrue((iField.getModifiers() & Modifier.PRIVATE) != 0); assertTrue((iField.getModifiers() & Modifier.FINAL) != 0); } @@ -107,15 +110,15 @@ public class RecordMemberTests { // methods are present, of the right descriptor, and public/instance/concrete for (String s : List.of("i", "j")) { Method iMethod = R1.class.getDeclaredMethod(s); - assertEquals(iMethod.getReturnType(), int.class); - assertEquals(iMethod.getParameterCount(), 0); - assertEquals((iMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT)), 0); + assertEquals(int.class, iMethod.getReturnType()); + assertEquals(0, iMethod.getParameterCount()); + assertEquals(0, (iMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT))); } Constructor c = R1.class.getConstructor(int.class, int.class); R1 r1 = (R1) c.newInstance(1, 2); - assertEquals(r1.i(), 1); - assertEquals(r1.j(), 2); + assertEquals(1, r1.i()); + assertEquals(2, r1.j()); } record OrdinaryMembers(int x) { @@ -124,21 +127,23 @@ public class RecordMemberTests { public String sf () { return "instance"; } } + @Test public void testOrdinaryMembers() { OrdinaryMembers.ss = "foo"; - assertEquals(OrdinaryMembers.ssf(), "foo"); + assertEquals("foo", OrdinaryMembers.ssf()); OrdinaryMembers o = new OrdinaryMembers(3); - assertEquals(o.sf(), "instance"); + assertEquals("instance", o.sf()); } class LocalRecordHelper { Class m(int x) { record R (int x) { } - assertEquals(new R(x).x(), x); + assertEquals(x, new R(x).x()); return R.class; } } + @Test public void testLocalRecordsStatic() { Class c = new LocalRecordHelper().m(3); String message = c.toGenericString(); @@ -181,6 +186,7 @@ public class RecordMemberTests { } } + @Test public void testNestedRecordsStatic() { NestedRecordHelper n = new NestedRecordHelper(); for (Class c : List.of(NestedRecordHelper.R1.class, diff --git a/test/langtools/tools/javac/records/VarargsRecordsTest.java b/test/langtools/tools/javac/records/VarargsRecordsTest.java index 14b9a9481d2..60c6e70d3ad 100644 --- a/test/langtools/tools/javac/records/VarargsRecordsTest.java +++ b/test/langtools/tools/javac/records/VarargsRecordsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,16 +27,15 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; -import org.testng.annotations.Test; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; /** * @test * @bug 8246774 * @summary test for varargs record components - * @run testng VarargsRecordsTest + * @run junit VarargsRecordsTest */ -@Test public class VarargsRecordsTest { public record RI(int... xs) { } public record RII(int x, int... xs) { } @@ -49,49 +48,52 @@ public class VarargsRecordsTest { RII r5 = new RII(1, 2); RII r6 = new RII(1, 2, 3); + @Test public void assertVarargsInstances() { - assertEquals(r1.xs.length, 0); - assertEquals(r2.xs.length, 1); - assertEquals(r3.xs.length, 2); - assertEquals(r4.xs.length, 0); - assertEquals(r5.xs.length, 1); - assertEquals(r6.xs.length, 2); + assertEquals(0, r1.xs.length); + assertEquals(1, r2.xs.length); + assertEquals(2, r3.xs.length); + assertEquals(0, r4.xs.length); + assertEquals(1, r5.xs.length); + assertEquals(2, r6.xs.length); - assertEquals(r2.xs[0], 1); - assertEquals(r3.xs[0], 1); - assertEquals(r3.xs[1], 2); + assertEquals(1, r2.xs[0]); + assertEquals(1, r3.xs[0]); + assertEquals(2, r3.xs[1]); - assertEquals(r5.xs[0], 2); - assertEquals(r6.xs[0], 2); - assertEquals(r6.xs[1], 3); + assertEquals(2, r5.xs[0]); + assertEquals(2, r6.xs[0]); + assertEquals(3, r6.xs[1]); } + @Test public void testMembers() throws ReflectiveOperationException { Constructor c = RI.class.getConstructor(int[].class); assertNotNull(c); assertTrue(c.isVarArgs()); Parameter[] parameters = c.getParameters(); - assertEquals(parameters.length, 1); - assertEquals(parameters[0].getName(), "xs"); + assertEquals(1, parameters.length); + assertEquals("xs", parameters[0].getName()); RI ri = (RI) c.newInstance(new int[]{1, 2}); - assertEquals(ri.xs()[0], 1); - assertEquals(ri.xs()[1], 2); + assertEquals(1, ri.xs()[0]); + assertEquals(2, ri.xs()[1]); Field xsField = RI.class.getDeclaredField("xs"); - assertEquals(xsField.getType(), int[].class); - assertEquals((xsField.getModifiers() & Modifier.STATIC), 0); + assertEquals(int[].class, xsField.getType()); + assertEquals(0, (xsField.getModifiers() & Modifier.STATIC)); assertTrue((xsField.getModifiers() & Modifier.PRIVATE) != 0); assertTrue((xsField.getModifiers() & Modifier.FINAL) != 0); - assertEquals(((int[]) xsField.get(ri))[0], 1); + assertEquals(1, ((int[]) xsField.get(ri))[0]); Method xsMethod = RI.class.getDeclaredMethod("xs"); - assertEquals(xsMethod.getReturnType(), int[].class); - assertEquals(xsMethod.getParameterCount(), 0); - assertEquals((xsMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT)), 0); - assertEquals(((int[]) xsMethod.invoke(ri))[0], 1); + assertEquals(int[].class, xsMethod.getReturnType()); + assertEquals(0, xsMethod.getParameterCount()); + assertEquals(0, (xsMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT))); + assertEquals(1, ((int[]) xsMethod.invoke(ri))[0]); } + @Test public void testNotVarargs() throws ReflectiveOperationException { Constructor c = RX.class.getConstructor(int[].class); assertFalse(c.isVarArgs()); diff --git a/test/langtools/tools/javac/sym/ElementStructureTest.java b/test/langtools/tools/javac/sym/ElementStructureTest.java index f1cc3cd91fe..14fa194501a 100644 --- a/test/langtools/tools/javac/sym/ElementStructureTest.java +++ b/test/langtools/tools/javac/sym/ElementStructureTest.java @@ -132,10 +132,10 @@ public class ElementStructureTest { (byte) 0x3D, (byte) 0xC1, (byte) 0xFE, (byte) 0xCB }; static final byte[] hash8 = new byte[] { - (byte) 0x10, (byte) 0xE6, (byte) 0xE8, (byte) 0x11, - (byte) 0xC8, (byte) 0x02, (byte) 0x63, (byte) 0x9B, - (byte) 0xAB, (byte) 0x11, (byte) 0x9E, (byte) 0x4F, - (byte) 0xFA, (byte) 0x00, (byte) 0x6D, (byte) 0x81 + (byte) 0x07, (byte) 0xAB, (byte) 0x0A, (byte) 0x8D, + (byte) 0x1C, (byte) 0x44, (byte) 0x6D, (byte) 0x71, + (byte) 0x07, (byte) 0x53, (byte) 0x59, (byte) 0x74, + (byte) 0x5B, (byte) 0x49, (byte) 0x60, (byte) 0xAD }; final static Map version2Hash = new HashMap<>(); @@ -283,7 +283,7 @@ public class ElementStructureTest { } JavaFileObject file = new ByteArrayJavaFileObject(data.toByteArray()); try (InputStream in = new ByteArrayInputStream(data.toByteArray())) { - String name = ClassFile.of().parse(in.readAllBytes()).thisClass().name().stringValue(); + String name = ClassFile.of().parse(in.readAllBytes()).thisClass().name().stringValue().replace("/", "."); className2File.put(name, file); file2ClassName.put(file, name); } catch (IOException ex) { @@ -514,6 +514,8 @@ public class ElementStructureTest { out.write(Double.toHexString((Double) value)); } else if (value instanceof Float) { out.write(Float.toHexString((Float) value)); + } else if (value instanceof Character ch && Character.isSurrogate(ch)) { + out.write(Integer.toHexString(ch)); } else { out.write(String.valueOf(value)); } diff --git a/test/langtools/tools/javac/tree/T8024415.java b/test/langtools/tools/javac/tree/T8024415.java index ef4b0429a81..a869f63f079 100644 --- a/test/langtools/tools/javac/tree/T8024415.java +++ b/test/langtools/tools/javac/tree/T8024415.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,16 +29,15 @@ * @modules jdk.compiler/com.sun.tools.javac.file * jdk.compiler/com.sun.tools.javac.tree * jdk.compiler/com.sun.tools.javac.util - * @run testng T8024415 + * @run junit T8024415 */ -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.io.StringWriter; -import org.testng.annotations.Test; import com.sun.tools.javac.file.JavacFileManager; import com.sun.tools.javac.tree.JCTree; @@ -47,13 +46,13 @@ import com.sun.tools.javac.tree.Pretty; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Names; +import org.junit.jupiter.api.Test; /* * Test verifies that the precedence rules of conditional expressions * (JCConditional) are correct. */ -@Test public class T8024415 { TreeMaker maker; @@ -72,6 +71,7 @@ public class T8024415 { // JLS 15.25: The conditional operator is syntactically right-associative // (it groups right-to-left). Thus, a?b:c?d:e?f:g means the same as // a?b:(c?d:(e?f:g)). + @Test public void testAssociativity() throws IOException { JCTree left = maker.Conditional(maker.Conditional(x, x, x), x, x); @@ -80,8 +80,8 @@ public class T8024415 { String prettyLeft = prettyPrint(left); String prettyRight = prettyPrint(right); - assertEquals(prettyLeft.replaceAll("\\s", ""), "(x?x:x)?x:x"); - assertEquals(prettyRight.replaceAll("\\s", ""), "x?x:x?x:x"); + assertEquals("(x?x:x)?x:x", prettyLeft.replaceAll("\\s", "")); + assertEquals("x?x:x?x:x", prettyRight.replaceAll("\\s", "")); } @@ -89,6 +89,7 @@ public class T8024415 { // The true-part of a conditional expression is surrounded by ? and : // and can thus always be parsed unambiguously without surrounding // parentheses. + @Test public void testPrecedence() throws IOException { JCTree left = maker.Conditional(maker.Assign(x, x), x, x); @@ -99,9 +100,9 @@ public class T8024415 { String prettyMiddle = prettyPrint(middle); String prettyRight = prettyPrint(right); - assertEquals(prettyLeft.replaceAll("\\s", ""), "(x=x)?x:x"); - assertEquals(prettyMiddle.replaceAll("\\s", ""), "x?x=x:x"); - assertEquals(prettyRight.replaceAll("\\s", ""), "x?x:(x=x)"); + assertEquals("(x=x)?x:x", prettyLeft.replaceAll("\\s", "")); + assertEquals("x?x=x:x", prettyMiddle.replaceAll("\\s", "")); + assertEquals("x?x:(x=x)", prettyRight.replaceAll("\\s", "")); } diff --git a/test/langtools/tools/javac/tree/TreePosTest.java b/test/langtools/tools/javac/tree/TreePosTest.java index 0ae62ca940d..9e6dcf61306 100644 --- a/test/langtools/tools/javac/tree/TreePosTest.java +++ b/test/langtools/tools/javac/tree/TreePosTest.java @@ -38,16 +38,11 @@ import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.nio.charset.Charset; +import java.io.UncheckedIOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; @@ -71,7 +66,7 @@ import javax.tools.StandardJavaFileManager; import com.sun.source.tree.CaseTree.CaseKind; import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; +import com.sun.tools.javac.api.JavacTaskPool; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.EndPosTable; @@ -80,7 +75,6 @@ import com.sun.tools.javac.tree.JCTree.JCAnnotatedType; import com.sun.tools.javac.tree.JCTree.JCCase; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; -import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.tree.JCTree.JCImportBase; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCNewClass; @@ -90,7 +84,6 @@ import com.sun.tools.javac.tree.TreeScanner; import static com.sun.tools.javac.tree.JCTree.Tag.*; import static com.sun.tools.javac.util.Position.NOPOS; -import java.util.stream.Stream; /** * Utility and test program to check validity of tree positions for tree nodes. @@ -275,6 +268,7 @@ public class TreePosTest { PrintWriter pw = new PrintWriter(sw); Reporter r = new Reporter(pw); JavacTool tool = JavacTool.create(); + JavacTaskPool pool = new JavacTaskPool(1); StandardJavaFileManager fm = tool.getStandardFileManager(r, null, null); /** @@ -285,21 +279,25 @@ public class TreePosTest { * @throws TreePosTest.ParseException if any errors occur while parsing the file */ JCCompilationUnit read(File file) throws IOException, ParseException { - JavacTool tool = JavacTool.create(); r.errors = 0; Iterable files = fm.getJavaFileObjects(file); - JavacTask task = tool.getTask(pw, fm, r, List.of("-proc:none"), null, files); - Iterable trees = task.parse(); - pw.flush(); - if (r.errors > 0) - throw new ParseException(sw.toString()); - Iterator iter = trees.iterator(); - if (!iter.hasNext()) - throw new Error("no trees found"); - JCCompilationUnit t = (JCCompilationUnit) iter.next(); - if (iter.hasNext()) - throw new Error("too many trees found"); - return t; + return pool.getTask(pw, fm, r, List.of("-proc:none"), null, files, task -> { + try { + Iterable trees = task.parse(); + pw.flush(); + if (r.errors > 0) + throw new ParseException(sw.toString()); + Iterator iter = trees.iterator(); + if (!iter.hasNext()) + throw new Error("no trees found"); + JCCompilationUnit t = (JCCompilationUnit) iter.next(); + if (iter.hasNext()) + throw new Error("too many trees found"); + return t; + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); } /** @@ -546,7 +544,7 @@ public class TreePosTest { /** * Thrown when errors are found parsing a java file. */ - private static class ParseException extends Exception { + private static class ParseException extends RuntimeException { ParseException(String msg) { super(msg); } diff --git a/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java b/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java index eb7953a0baf..929d2c58685 100644 --- a/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java +++ b/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,14 +24,14 @@ /* * @test * @bug 8209022 - * @run testng TypeVariableCastTest + * @run junit TypeVariableCastTest * @summary Missing checkcast when casting to type parameter bounded by intersection type */ import java.util.*; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -@Test public class TypeVariableCastTest { static & Runnable> void f() { Runnable r = (T) new ArrayList<>(); @@ -41,13 +41,17 @@ public class TypeVariableCastTest { Runnable r = (T) new ArrayList<>(); } - @Test(expectedExceptions = ClassCastException.class) - static void testMethodF() { - f(); + @Test + void testMethodF() { + Assertions.assertThrows(ClassCastException.class, () -> { + f(); + }); } - @Test(expectedExceptions = ClassCastException.class) - static void testMethodG() { - g(); + @Test + void testMethodG() { + Assertions.assertThrows(ClassCastException.class, () -> { + g(); + }); } } diff --git a/test/langtools/tools/javac/warnings/WerrorLint.e1.out b/test/langtools/tools/javac/warnings/WerrorLint.e1.out new file mode 100644 index 00000000000..ae99cfa5056 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.e1.out @@ -0,0 +1,4 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +- compiler.err.warnings.and.werror +1 error +1 warning diff --git a/test/langtools/tools/javac/warnings/WerrorLint.e2.out b/test/langtools/tools/javac/warnings/WerrorLint.e2.out new file mode 100644 index 00000000000..1c9bd4d54f8 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.e2.out @@ -0,0 +1,5 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +WerrorLint.java:21:30: compiler.warn.empty.if +- compiler.err.warnings.and.werror +1 error +2 warnings diff --git a/test/langtools/tools/javac/warnings/WerrorLint.java b/test/langtools/tools/javac/warnings/WerrorLint.java new file mode 100644 index 00000000000..3331a664d55 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.java @@ -0,0 +1,23 @@ +/* + * @test /nodynamiccopyright/ + * @bug 8349847 + * + * @compile -XDrawDiagnostics -Xlint:none WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror:empty WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror:strictfp WerrorLint.java + * @compile/ref=WerrorLint.w2.out -XDrawDiagnostics -Xlint:all WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror:empty WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror:strictfp WerrorLint.java + * @compile/ref=WerrorLint.w1.out -XDrawDiagnostics WerrorLint.java + * @compile/fail/ref=WerrorLint.e1.out -XDrawDiagnostics -Werror WerrorLint.java + * @compile/ref=WerrorLint.w1.out -XDrawDiagnostics -Werror:empty WerrorLint.java + * @compile/fail/ref=WerrorLint.e1.out -XDrawDiagnostics -Werror:strictfp WerrorLint.java + */ + +class WerrorLint { + strictfp void m() { // [strictfp] - this category is enabled by default + if (hashCode() == 1) ; // [empty] - this category is disabled by default + } +} diff --git a/test/langtools/tools/javac/warnings/WerrorLint.w1.out b/test/langtools/tools/javac/warnings/WerrorLint.w1.out new file mode 100644 index 00000000000..3e19de51033 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.w1.out @@ -0,0 +1,2 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +1 warning diff --git a/test/langtools/tools/javac/warnings/WerrorLint.w2.out b/test/langtools/tools/javac/warnings/WerrorLint.w2.out new file mode 100644 index 00000000000..bac258706a6 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.w2.out @@ -0,0 +1,3 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +WerrorLint.java:21:30: compiler.warn.empty.if +2 warnings diff --git a/test/lib-test/TEST.ROOT b/test/lib-test/TEST.ROOT index 162e6e15ec2..ebdf3f1a334 100644 --- a/test/lib-test/TEST.ROOT +++ b/test/lib-test/TEST.ROOT @@ -29,7 +29,7 @@ keys=randomness # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8.1+1 # Allow querying of various System properties in @requires clauses requires.extraPropDefns = ../jtreg-ext/requires/VMProps.java diff --git a/test/lib/jdk/test/lib/Utils.java b/test/lib/jdk/test/lib/Utils.java index c4a42dc61ba..2f46ed87340 100644 --- a/test/lib/jdk/test/lib/Utils.java +++ b/test/lib/jdk/test/lib/Utils.java @@ -38,7 +38,6 @@ import java.net.ServerSocket; import java.net.URL; import java.net.URLClassLoader; import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.CopyOption; import java.nio.file.Files; @@ -50,8 +49,6 @@ import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.FileAttribute; import java.nio.channels.SocketChannel; import java.nio.file.attribute.PosixFilePermissions; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -71,6 +68,7 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.zip.CRC32C; import static java.lang.System.lineSeparator; import static jdk.test.lib.Asserts.assertTrue; @@ -170,16 +168,11 @@ public final class Utils { var v = Runtime.version(); // promotable builds have build number, and it's greater than 0 if (v.build().orElse(0) > 0) { - // promotable build -> use 1st 8 bytes of md5($version) - try { - var md = MessageDigest.getInstance("MD5"); - var bytes = v.toString() - .getBytes(StandardCharsets.UTF_8); - bytes = md.digest(bytes); - SEED = ByteBuffer.wrap(bytes).getLong(); - } catch (NoSuchAlgorithmException e) { - throw new Error(e); - } + // promotable build -> generate a seed based on the version string + var bytes = v.toString().getBytes(StandardCharsets.UTF_8); + var crc = new CRC32C(); + crc.update(bytes); + SEED = crc.getValue(); } else { // "personal" build -> use random seed SEED = new Random().nextLong(); diff --git a/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java b/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java index 83e381356a0..fb67ce83bbe 100644 --- a/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java +++ b/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java @@ -23,8 +23,7 @@ package jdk.test.lib.artifacts; -import jtreg.SkippedException; - +import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -90,15 +89,15 @@ public class ArtifactResolver { * @return the local path to the artifact. If the artifact is a compressed * file that gets unpacked, this path will point to the root * directory of the uncompressed file(s). - * @throws SkippedException thrown if the artifact cannot be found + * @throws IOException thrown if the artifact cannot be found */ - public static Path fetchOne(Class klass) { + public static Path fetchOne(Class klass) throws IOException { try { return ArtifactResolver.resolve(klass).entrySet().stream() .findAny().get().getValue(); } catch (ArtifactResolverException e) { Artifact artifact = klass.getAnnotation(Artifact.class); - throw new SkippedException("Cannot find the artifact " + artifact.name(), e); + throw new IOException("Cannot find the artifact " + artifact.name(), e); } } diff --git a/test/lib/jdk/test/lib/net/SimpleHttpServer.java b/test/lib/jdk/test/lib/net/SimpleHttpServer.java deleted file mode 100644 index 1905091eac6..00000000000 --- a/test/lib/jdk/test/lib/net/SimpleHttpServer.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package jdk.test.lib.net; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -/** - * A simple HTTP Server. - **/ -public class SimpleHttpServer { - private final HttpServer httpServer; - private ExecutorService executor; - private String address; - private final String context; - private final String docRoot; - private final InetSocketAddress inetSocketAddress; - - public SimpleHttpServer(final InetSocketAddress inetSocketAddress, final String context, final String docRoot) - throws IOException { - this.inetSocketAddress = inetSocketAddress; - this.context = context; - this.docRoot = docRoot; - httpServer = HttpServer.create(); - } - - public void start() throws IOException, URISyntaxException { - MyHttpHandler handler = new MyHttpHandler(docRoot); - httpServer.bind(inetSocketAddress, 0); - httpServer.createContext(context, handler); - executor = Executors.newCachedThreadPool(); - httpServer.setExecutor(executor); - httpServer.start(); - address = "http:" + URIBuilder.newBuilder().host(httpServer.getAddress().getAddress()). - port(httpServer.getAddress().getPort()).build().toString(); - } - - public void stop() { - httpServer.stop(0); - executor.shutdown(); - } - - public String getAddress() { - return address; - } - - public int getPort() { - return httpServer.getAddress().getPort(); - } - - class MyHttpHandler implements HttpHandler { - private final URI rootUri; - - MyHttpHandler(final String docroot) { - rootUri = Path.of(docroot).toUri().normalize(); - } - - public void handle(final HttpExchange t) throws IOException { - try (InputStream is = t.getRequestBody()) { - is.readAllBytes(); - Headers rMap = t.getResponseHeaders(); - try (OutputStream os = t.getResponseBody()) { - URI uri = t.getRequestURI(); - String path = uri.getRawPath(); - assert path.isEmpty() || path.startsWith("/"); - Path fPath; - try { - uri = URI.create("file://" + rootUri.getRawPath() + path).normalize(); - fPath = Path.of(uri); - } catch (IllegalArgumentException | FileSystemNotFoundException ex) { - ex.printStackTrace(); - notfound(t, path); - return; - } - byte[] bytes = Files.readAllBytes(fPath); - String method = t.getRequestMethod(); - if (method.equals("HEAD")) { - rMap.set("Content-Length", Long.toString(bytes.length)); - t.sendResponseHeaders(200, -1); - t.close(); - } else if (!method.equals("GET")) { - t.sendResponseHeaders(405, -1); - t.close(); - return; - } - if (path.endsWith(".html") || path.endsWith(".htm")) { - rMap.set("Content-Type", "text/html"); - } else { - rMap.set("Content-Type", "text/plain"); - } - t.sendResponseHeaders(200, bytes.length); - os.write(bytes); - } - } - } - void moved(final HttpExchange t) throws IOException { - Headers req = t.getRequestHeaders(); - Headers map = t.getResponseHeaders(); - URI uri = t.getRequestURI(); - String host = req.getFirst("Host"); - String location = "http://" + host + uri.getPath() + "/"; - map.set("Content-Type", "text/html"); - map.set("Location", location); - t.sendResponseHeaders(301, -1); - t.close(); - } - void notfound(final HttpExchange t, final String p) throws IOException { - t.getResponseHeaders().set("Content-Type", "text/html"); - t.sendResponseHeaders(404, 0); - try (OutputStream os = t.getResponseBody()) { - String s = "

    File not found

    "; - s = s + p + "

    "; - os.write(s.getBytes()); - } - t.close(); - } - } -} diff --git a/test/lib/jdk/test/lib/os/linux/Smaps.java b/test/lib/jdk/test/lib/os/linux/Smaps.java new file mode 100644 index 00000000000..11cdfe34319 --- /dev/null +++ b/test/lib/jdk/test/lib/os/linux/Smaps.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.test.lib.os.linux; + +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Smaps { + + // List of memory ranges + private List ranges; + + protected Smaps(List ranges) { + this.ranges = ranges; + } + + // Search for a range including the given address. + public Range getRange(String addr) { + BigInteger laddr = new BigInteger(addr.substring(2), 16); + for (Range range : ranges) { + if (range.includes(laddr)) { + return range; + } + } + + return null; + } + + public static Smaps parseSelf() throws Exception { + return parse(Path.of("/proc/self/smaps")); + } + + public static Smaps parse(Path smaps) throws Exception { + return new Parser(smaps).parse(); + } + + // This is a simple smaps parser; it will recognize smaps section start lines + // (e.g. "40fa00000-439b80000 rw-p 00000000 00:00 0 ") and look for keywords inside the section. + // Section will be finished and written into a RangeWithPageSize when either the next section is found + // or the end of file is encountered. + private static class Parser { + + private static final Pattern SECTION_START_PATT = Pattern.compile("^([a-f0-9]+)-([a-f0-9]+) [\\-rwpsx]{4}.*"); + private static final Pattern KERNEL_PAGESIZE_PATT = Pattern.compile("^KernelPageSize:\\s*(\\d*) kB"); + private static final Pattern THP_ELIGIBLE_PATT = Pattern.compile("^THPeligible:\\s+(\\d*)"); + private static final Pattern VMFLAGS_PATT = Pattern.compile("^VmFlags: ([\\w\\? ]*)"); + + String start; + String end; + String ps; + String thpEligible; + String vmFlags; + + List ranges; + Path smaps; + + Parser(Path smaps) { + this.ranges = new LinkedList(); + this.smaps = smaps; + reset(); + } + + void reset() { + start = null; + end = null; + ps = null; + thpEligible = null; + vmFlags = null; + } + + public void finish() { + if (start != null) { + Range range = new Range(start, end, ps, thpEligible, vmFlags); + ranges.add(range); + reset(); + } + } + + public void eatNext(String line) { + // For better debugging experience call finish here before the debug() call. + Matcher matSectionStart = SECTION_START_PATT.matcher(line); + if (matSectionStart.matches()) { + finish(); + } + + if (matSectionStart.matches()) { + start = matSectionStart.group(1); + end = matSectionStart.group(2); + ps = null; + vmFlags = null; + return; + } else { + Matcher matKernelPageSize = KERNEL_PAGESIZE_PATT.matcher(line); + if (matKernelPageSize.matches()) { + ps = matKernelPageSize.group(1); + return; + } + Matcher matTHPEligible = THP_ELIGIBLE_PATT.matcher(line); + if (matTHPEligible.matches()) { + thpEligible = matTHPEligible.group(1); + return; + } + Matcher matVmFlags = VMFLAGS_PATT.matcher(line); + if (matVmFlags.matches()) { + vmFlags = matVmFlags.group(1); + return; + } + } + } + + // Copy smaps locally + // (To minimize chances of concurrent modification when parsing, as well as helping with error analysis) + private Path copySmaps() throws Exception { + Path copy = Paths.get("smaps-copy-" + ProcessHandle.current().pid() + "-" + System.nanoTime() + ".txt"); + Files.copy(smaps, copy, StandardCopyOption.REPLACE_EXISTING); + return copy; + } + + // Parse /proc/self/smaps + public Smaps parse() throws Exception { + Path smapsCopy = copySmaps(); + Files.lines(smapsCopy).forEach(this::eatNext); + + // Finish up the last range + this.finish(); + + // Return a Smaps object with the parsed ranges + return new Smaps(ranges); + } + } + + // Class used to store information about memory ranges parsed + // from /proc/self/smaps. The file contain a lot of information + // about the different mappings done by an application, but the + // lines we care about are: + // 700000000-73ea00000 rw-p 00000000 00:00 0 + // ... + // KernelPageSize: 4 kB + // ... + // THPeligible: 0 + // ... + // VmFlags: rd wr mr mw me ac sd + // + // We use the VmFlags to know what kind of huge pages are used. + // For transparent huge pages the KernelPageSize field will not + // report the large page size. + public static class Range { + + private BigInteger start; + private BigInteger end; + private long pageSize; + private boolean thpEligible; + private boolean vmFlagHG; + private boolean vmFlagHT; + private boolean isTHP; + + public Range(String start, String end, String pageSize, String thpEligible, String vmFlags) { + // Note: since we insist on kernels >= 3.8, all the following information should be present + // (none of the input strings be null). + this.start = new BigInteger(start, 16); + this.end = new BigInteger(end, 16); + this.pageSize = Long.parseLong(pageSize); + this.thpEligible = thpEligible == null ? false : (Integer.parseInt(thpEligible) == 1); + + vmFlagHG = false; + vmFlagHT = false; + // Check if the vmFlags line include: + // * ht - Meaning the range is mapped using explicit huge pages. + // * hg - Meaning the range is madvised huge. + for (String flag : vmFlags.split(" ")) { + if (flag.equals("ht")) { + vmFlagHT = true; + } else if (flag.equals("hg")) { + vmFlagHG = true; + } + } + + // When the THP policy is 'always' instead of 'madvise, the vmFlagHG property is false, + // therefore also check thpEligible. If this is still causing problems in the future, + // we might have to check the AnonHugePages field. + + isTHP = vmFlagHG || this.thpEligible; + } + + public BigInteger getStart() { + return start; + } + + public BigInteger getEnd() { + return end; + } + + public long getPageSize() { + return pageSize; + } + + public boolean isTransparentHuge() { + return isTHP; + } + + public boolean isExplicitHuge() { + return vmFlagHT; + } + + public boolean includes(BigInteger addr) { + boolean isGreaterThanOrEqualStart = start.compareTo(addr) <= 0; + boolean isLessThanEnd = addr.compareTo(end) < 0; + + return isGreaterThanOrEqualStart && isLessThanEnd; + } + + public String toString() { + return "[" + start.toString(16) + ", " + end.toString(16) + ") " + + "pageSize=" + pageSize + "KB isTHP=" + isTHP + " isHUGETLB=" + vmFlagHT; + } + } +} diff --git a/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java b/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java index 82252000154..6fa8cb2e408 100644 --- a/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java +++ b/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java @@ -23,6 +23,7 @@ package jdk.test.lib.security; +import java.io.IOException; import java.nio.file.Path; import jdk.test.lib.Platform; import jdk.test.lib.process.ProcessTools; @@ -49,42 +50,40 @@ public class OpensslArtifactFetcher { * * @return openssl binary path of the current version * @throws SkippedException if a valid version of OpenSSL cannot be found + * or if OpenSSL is not available on the target platform */ public static String getOpensslPath() { String path = getOpensslFromSystemProp(OPENSSL_BUNDLE_VERSION); if (path != null) { + System.out.println("Using OpenSSL from system property."); return path; } + path = getDefaultSystemOpensslPath(OPENSSL_BUNDLE_VERSION); if (path != null) { + System.out.println("Using OpenSSL from system."); return path; } + if (Platform.isX64()) { if (Platform.isLinux()) { - path = fetchOpenssl(LINUX_X64.class); + return fetchOpenssl(LINUX_X64.class); } else if (Platform.isOSX()) { - path = fetchOpenssl(MACOSX_X64.class); + return fetchOpenssl(MACOSX_X64.class); } else if (Platform.isWindows()) { - path = fetchOpenssl(WINDOWS_X64.class); + return fetchOpenssl(WINDOWS_X64.class); } } else if (Platform.isAArch64()) { if (Platform.isLinux()) { - path = fetchOpenssl(LINUX_AARCH64.class); + return fetchOpenssl(LINUX_AARCH64.class); } if (Platform.isOSX()) { - path = fetchOpenssl(MACOSX_AARCH64.class); + return fetchOpenssl(MACOSX_AARCH64.class); } } - if (!verifyOpensslVersion(path, OPENSSL_BUNDLE_VERSION)) { - String exMsg = "Can't find the version: " - + OpensslArtifactFetcher.getTestOpensslBundleVersion() - + " of openssl binary on this machine, please install" - + " and set openssl path with property 'test.openssl.path'"; - throw new SkippedException(exMsg); - } else { - return path; - } + throw new SkippedException(String.format("No OpenSSL %s found for %s/%s", + OPENSSL_BUNDLE_VERSION, Platform.getOsName(), Platform.getOsArch())); } private static String getOpensslFromSystemProp(String version) { @@ -120,9 +119,13 @@ public class OpensslArtifactFetcher { } private static String fetchOpenssl(Class clazz) { - return ArtifactResolver.fetchOne(clazz) - .resolve("openssl", "bin", "openssl") - .toString(); + try { + return ArtifactResolver.fetchOne(clazz) + .resolve("openssl", "bin", "openssl") + .toString(); + } catch (IOException exc) { + throw new SkippedException("Could not find openssl", exc); + } } // retrieve the provider directory path from /bin/openssl diff --git a/test/lib/jdk/test/lib/threaddump/ThreadDump.java b/test/lib/jdk/test/lib/threaddump/ThreadDump.java index f4964a9521f..ca728e625fc 100644 --- a/test/lib/jdk/test/lib/threaddump/ThreadDump.java +++ b/test/lib/jdk/test/lib/threaddump/ThreadDump.java @@ -296,6 +296,16 @@ public final class ThreadDump { return getStringProperty("parkBlocker", "object"); } + /** + * Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer. + */ + public OptionalLong parkBlockerOwner() { + String owner = getStringProperty("parkBlocker", "owner"); + return (owner != null) + ? OptionalLong.of(Long.parseLong(owner)) + : OptionalLong.empty(); + } + /** * Returns the object that the thread is blocked entering its monitor. */ diff --git a/test/lib/jdk/test/whitebox/WhiteBox.java b/test/lib/jdk/test/whitebox/WhiteBox.java index 5adb7bf5127..d07dedd2a8c 100644 --- a/test/lib/jdk/test/whitebox/WhiteBox.java +++ b/test/lib/jdk/test/whitebox/WhiteBox.java @@ -490,6 +490,12 @@ public class WhiteBox { Objects.requireNonNull(method); return getNMethod0(method, isOsr); } + private native void relocateNMethodFromMethod0(Executable method, int type); + public void relocateNMethodFromMethod(Executable method, int type) { + Objects.requireNonNull(method); + relocateNMethodFromMethod0(method, type); + } + public native void relocateNMethodFromAddr(long address, int type); public native long allocateCodeBlob(int size, int type); public long allocateCodeBlob(long size, int type) { int intSize = (int) size; @@ -841,13 +847,12 @@ public class WhiteBox { public native void waitUnsafe(int time_ms); - public native void busyWait(int cpuTimeMs); + public native void busyWaitCPUTime(int cpuTimeMs); + // returns true if supported, false if not public native boolean cpuSamplerSetOutOfStackWalking(boolean enable); - public native long cpuSamplerOutOfStackWalkingIterations(); - public native void pinObject(Object o); public native void unpinObject(Object o); diff --git a/test/lib/jdk/test/whitebox/code/CodeBlob.java b/test/lib/jdk/test/whitebox/code/CodeBlob.java index c6c23fdff0c..fd95b5a7e7d 100644 --- a/test/lib/jdk/test/whitebox/code/CodeBlob.java +++ b/test/lib/jdk/test/whitebox/code/CodeBlob.java @@ -46,18 +46,22 @@ public class CodeBlob { return new CodeBlob(obj); } protected CodeBlob(Object[] obj) { - assert obj.length == 4; + assert obj.length == 6; name = (String) obj[0]; size = (Integer) obj[1]; int blob_type_index = (Integer) obj[2]; code_blob_type = BlobType.values()[blob_type_index]; assert code_blob_type.id == (Integer) obj[2]; address = (Long) obj[3]; + code_begin = (Long) obj[4]; + isNMethod = (Boolean) obj[5]; } public final String name; public final int size; public final BlobType code_blob_type; public final long address; + public final long code_begin; + public final boolean isNMethod; @Override public String toString() { return "CodeBlob{" @@ -65,6 +69,7 @@ public class CodeBlob { + ", size=" + size + ", code_blob_type=" + code_blob_type + ", address=" + address + + ", code_begin=" + code_begin + '}'; } } diff --git a/test/micro/org/openjdk/bench/java/lang/ClassLoaderDefineClass.java b/test/micro/org/openjdk/bench/java/lang/ClassLoaderDefineClass.java new file mode 100644 index 00000000000..614b3fe7b20 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/ClassLoaderDefineClass.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.lang; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.lang.foreign.Arena; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import java.util.HexFormat; + +/** + * Tests java.lang.ClassLoader.defineClass(ByteBuffer) + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +public class ClassLoaderDefineClass { + + private byte[] classBytes; + private ByteBuffer directBuffer; + private ByteBuffer heapBuffer; + + @Setup(Level.Iteration) + public void setup() throws Exception { + classBytes = getTestClassBytes(); + directBuffer = Arena.ofConfined() + .allocate(classBytes.length) + .asByteBuffer() + .put(classBytes) + .flip(); + heapBuffer = ByteBuffer.wrap(classBytes); + } + + @Benchmark + public void testDefineClassByteBufferHeap(Blackhole bh) throws Exception { + bh.consume(new DummyClassLoader().defineClassFromHeapBuffer(heapBuffer)); + } + + @Benchmark + public void testDefineClassByteBufferDirect(Blackhole bh) throws Exception { + bh.consume(new DummyClassLoader().defineClassFromDirectBuffer(directBuffer)); + } + + private static final class DummyClassLoader extends ClassLoader { + + Class defineClassFromHeapBuffer(ByteBuffer bb) throws Exception { + bb.rewind(); + return defineClass(null, bb, null); + } + + Class defineClassFromDirectBuffer(ByteBuffer bb) throws Exception { + bb.rewind(); + return defineClass(null, bb, null); + } + } + + private static byte[] getTestClassBytes() throws Exception { + final String source = """ + public class Greeting { + public String hello() { + return "Hello"; + } + } + """; + // (externally) compiled content of the above source, represented as hex + final String classBytesHex = """ + cafebabe0000004600110a000200030700040c000500060100106a617661 + 2f6c616e672f4f626a6563740100063c696e69743e010003282956080008 + 01000548656c6c6f07000a0100084772656574696e67010004436f646501 + 000f4c696e654e756d6265725461626c6501000568656c6c6f0100142829 + 4c6a6176612f6c616e672f537472696e673b01000a536f7572636546696c + 6501000d4772656574696e672e6a61766100210009000200000000000200 + 01000500060001000b0000001d00010001000000052ab70001b100000001 + 000c000000060001000000010001000d000e0001000b0000001b00010001 + 000000031207b000000001000c000000060001000000030001000f000000 + 020010 + """; + return HexFormat.of().parseHex(classBytesHex.replaceAll("\n", "")); + } +} diff --git a/test/micro/org/openjdk/bench/java/lang/PopCountValueTransform.java b/test/micro/org/openjdk/bench/java/lang/PopCountValueTransform.java new file mode 100644 index 00000000000..c896c7504a6 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/PopCountValueTransform.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.lang; + +import org.openjdk.jmh.annotations.*; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class PopCountValueTransform { + public int lower_bound = 0; + public int upper_bound = 10000; + + @Benchmark + public int LogicFoldingKerenlInt() { + int res = 0; + for (int i = lower_bound; i < upper_bound; i++) { + int constrained_i = i & 0xFFFF; + if (Integer.bitCount(constrained_i) > 16) { + throw new AssertionError("Uncommon trap"); + } + res += constrained_i; + } + return res; + } + + @Benchmark + public long LogicFoldingKerenLong() { + long res = 0; + for (int i = lower_bound; i < upper_bound; i++) { + long constrained_i = i & 0xFFFFFFL; + if (Long.bitCount(constrained_i) > 24) { + throw new AssertionError("Uncommon trap"); + } + res += constrained_i; + } + return res; + } +} diff --git a/test/micro/org/openjdk/bench/java/nio/file/ToRealPath.java b/test/micro/org/openjdk/bench/java/nio/file/ToRealPath.java new file mode 100644 index 00000000000..23dba3858ba --- /dev/null +++ b/test/micro/org/openjdk/bench/java/nio/file/ToRealPath.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.nio.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.FileVisitResult; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Random; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Benchmark) +public class ToRealPath { + + static Random RND = new Random(17_000_126); + + static final String NAME = "RealPath"; + static final int LEN = NAME.length(); + + Path root; + Path[] files; + + @Setup + public void init() throws IOException { + // root the test files at CWD/NAME + root = Path.of(System.getProperty("user.dir")).resolve(NAME); + + // populate files array + StringBuilder sb = new StringBuilder(); + files = new Path[100]; + for (int i = 0; i < files.length; i++) { + // create directories up to a depth of 9, inclusive + sb.setLength(0); + int depth = RND.nextInt(10); + for (int j = 0; j < depth; j++) { + sb.append("dir"); + sb.append(j); + sb.append(File.separatorChar); + } + Path dir = root.resolve(sb.toString()); + Files.createDirectories(dir); + + // set the file prefix with random case conversion + String prefix; + if (RND.nextBoolean()) { + sb.setLength(0); + for (int k = 0; k < LEN; k++) { + char c = NAME.charAt(k); + sb.append(RND.nextBoolean() + ? Character.toLowerCase(c) + : Character.toUpperCase(c)); + } + prefix = sb.append(i).toString(); + } else { + prefix = NAME + i; + } + + // create the file + Path tmpFile = Files.createTempFile(dir, prefix, ".tmp"); + + // set the array path to a version with a lower case name + String tmpName = tmpFile.getFileName().toString().toLowerCase(); + files[i] = tmpFile.getParent().resolve(tmpName); + } + } + + @TearDown + public void cleanup() throws IOException { + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) + throws IOException + { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, + IOException e) + throws IOException + { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } + + @Benchmark + public Path noFollowLinks() throws IOException { + int i = RND.nextInt(0, files.length); + return files[i].toRealPath(LinkOption.NOFOLLOW_LINKS); + } +} diff --git a/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java b/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java index 1da18cc97a0..cd49469c15d 100644 --- a/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java +++ b/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,13 +22,16 @@ */ package org.openjdk.bench.java.text; +import java.math.BigDecimal; import java.text.NumberFormat; import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.stream.DoubleStream; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OperationsPerInvocation; @@ -50,25 +53,52 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Benchmark) public class DefFormatterBench { + public static final int VALUES_SIZE = 13; public double[] values; + public BigDecimal[] bdLargeValues; + public BigDecimal[] bdSmallValues; - @Setup + @Setup(Level.Invocation) public void setup() { values = new double[] { 1.23, 1.49, 1.80, 1.7, 0.0, -1.49, -1.50, 9999.9123, 1.494, 1.495, 1.03, 25.996, -25.996 }; + + bdLargeValues = DoubleStream.of(values) + .mapToObj(BigDecimal::new) + .toArray(BigDecimal[]::new); + + bdSmallValues = DoubleStream.of(values) + .mapToObj(BigDecimal::valueOf) + .toArray(BigDecimal[]::new); } private DefNumberFormat dnf = new DefNumberFormat(); @Benchmark - @OperationsPerInvocation(13) + @OperationsPerInvocation(VALUES_SIZE) public void testDefNumberFormatter(final Blackhole blackhole) { for (double value : values) { blackhole.consume(this.dnf.format(value)); } } + @Benchmark + @OperationsPerInvocation(VALUES_SIZE) + public void testSmallBigDecDefNumberFormatter(final Blackhole blackhole) { + for (BigDecimal value : bdSmallValues) { + blackhole.consume(this.dnf.format(value)); + } + } + + @Benchmark + @OperationsPerInvocation(VALUES_SIZE) + public void testLargeBigDecDefNumberFormatter(final Blackhole blackhole) { + for (BigDecimal value : bdLargeValues) { + blackhole.consume(this.dnf.format(value)); + } + } + public static void main(String... args) throws Exception { Options opts = new OptionsBuilder().include(DefFormatterBench.class.getSimpleName()).shouldDoGC(true).build(); new Runner(opts).run(); @@ -88,5 +118,9 @@ public class DefFormatterBench { public String format(final double d) { return this.n.format(d); } + + public String format(final BigDecimal bd) { + return this.n.format(bd); + } } } diff --git a/test/micro/org/openjdk/bench/java/text/DateFormatterBench.java b/test/micro/org/openjdk/bench/java/text/SimpleDateFormatterBench.java similarity index 60% rename from test/micro/org/openjdk/bench/java/text/DateFormatterBench.java rename to test/micro/org/openjdk/bench/java/text/SimpleDateFormatterBench.java index f9a6340a0fb..04c704f5ec8 100644 --- a/test/micro/org/openjdk/bench/java/text/DateFormatterBench.java +++ b/test/micro/org/openjdk/bench/java/text/SimpleDateFormatterBench.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -38,8 +39,9 @@ import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Date; -import java.util.Locale; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.Throughput) @@ -48,32 +50,62 @@ import java.util.concurrent.TimeUnit; @Measurement(iterations = 5, time = 1) @Fork(3) @State(Scope.Benchmark) -public class DateFormatterBench { +public class SimpleDateFormatterBench { private Date date; - private Object objDate; + private String dateStr; + private String timeStr; + + private static final String DATE_PATTERN = "EEEE, MMMM d, y"; + private static final String TIME_PATTERN = "h:mm:ss a zzzz"; + + // Use non-factory methods w/ pattern to ensure test data can be round + // tripped and guarantee no re-use of the same instance + private DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN); + private DateFormat timeFormat = new SimpleDateFormat(TIME_PATTERN); @Setup public void setup() { date = new Date(); objDate = new Date(); + // Generate the strings for parsing using dedicated separate instances + dateStr = new SimpleDateFormat(DATE_PATTERN).format(date); + timeStr = new SimpleDateFormat(TIME_PATTERN).format(date); } - private DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL, Locale.ENGLISH); + @Benchmark + public String testTimeFormat() { + return timeFormat.format(date); + } @Benchmark - public String testFormatDate() { + public String testTimeFormatObject() { + return timeFormat.format(objDate); + } + + @Benchmark + public String testDateFormat() { return dateFormat.format(date); } @Benchmark - public String testFormatObject() { + public String testDateFormatObject() { return dateFormat.format(objDate); } + @Benchmark + public Date testDateParse() throws ParseException { + return dateFormat.parse(dateStr); + } + + @Benchmark + public Date testTimeParse() throws ParseException { + return timeFormat.parse(timeStr); + } + public static void main(String... args) throws Exception { - Options opts = new OptionsBuilder().include(DateFormatterBench.class.getSimpleName()).shouldDoGC(true).build(); + Options opts = new OptionsBuilder().include(org.openjdk.bench.java.text.SimpleDateFormatterBench.class.getSimpleName()).shouldDoGC(true).build(); new Runner(opts).run(); } } diff --git a/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java b/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java new file mode 100644 index 00000000000..d83bc126a1d --- /dev/null +++ b/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.openjdk.bench.jdk.incubator.vector; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.*; + +import jdk.incubator.vector.*; +import java.lang.invoke.*; +import java.util.concurrent.TimeUnit; +import java.util.Random; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 2, jvmArgs = { "--add-modules=jdk.incubator.vector" }) +public abstract class MaskCompareNotBenchmark { + @Param({"4096"}) + protected int ARRAYLEN; + + // Abstract method to get comparison operator from subclasses + protected abstract String getComparisonOperatorName(); + + // To get compile-time constants for comparison operation + static final MutableCallSite MUTABLE_COMPARISON_CONSTANT = new MutableCallSite(MethodType.methodType(VectorOperators.Comparison.class)); + static final MethodHandle MUTABLE_COMPARISON_CONSTANT_HANDLE = MUTABLE_COMPARISON_CONSTANT.dynamicInvoker(); + + private static Random r = new Random(); + + protected static final VectorSpecies B_SPECIES = ByteVector.SPECIES_MAX; + protected static final VectorSpecies S_SPECIES = ShortVector.SPECIES_MAX; + protected static final VectorSpecies I_SPECIES = IntVector.SPECIES_MAX; + protected static final VectorSpecies L_SPECIES = LongVector.SPECIES_MAX; + protected static final VectorSpecies F_SPECIES = FloatVector.SPECIES_MAX; + protected static final VectorSpecies D_SPECIES = DoubleVector.SPECIES_MAX; + + protected boolean[] mr; + protected byte[] ba; + protected byte[] bb; + protected short[] sa; + protected short[] sb; + protected int[] ia; + protected int[] ib; + protected long[] la; + protected long[] lb; + protected float[] fa; + protected float[] fb; + protected double[] da; + protected double[] db; + + @Setup + public void init() throws Throwable { + mr = new boolean[ARRAYLEN]; + ba = new byte[ARRAYLEN]; + bb = new byte[ARRAYLEN]; + sa = new short[ARRAYLEN]; + sb = new short[ARRAYLEN]; + ia = new int[ARRAYLEN]; + ib = new int[ARRAYLEN]; + la = new long[ARRAYLEN]; + lb = new long[ARRAYLEN]; + fa = new float[ARRAYLEN]; + fb = new float[ARRAYLEN]; + da = new double[ARRAYLEN]; + db = new double[ARRAYLEN]; + + for (int i = 0; i < ARRAYLEN; i++) { + mr[i] = r.nextBoolean(); + ba[i] = (byte) r.nextInt(); + bb[i] = (byte) r.nextInt(); + sa[i] = (short) r.nextInt(); + sb[i] = (short) r.nextInt(); + ia[i] = r.nextInt(); + ib[i] = r.nextInt(); + la[i] = r.nextLong(); + lb[i] = r.nextLong(); + fa[i] = r.nextFloat(); + fb[i] = r.nextFloat(); + da[i] = r.nextDouble(); + db[i] = r.nextDouble(); + } + + VectorOperators.Comparison comparisonOp = getComparisonOperator(getComparisonOperatorName()); + MethodHandle constant = MethodHandles.constant(VectorOperators.Comparison.class, comparisonOp); + MUTABLE_COMPARISON_CONSTANT.setTarget(constant); + } + + @CompilerControl(CompilerControl.Mode.INLINE) + private static VectorOperators.Comparison getComparisonOperator(String op) { + switch (op) { + case "EQ": return VectorOperators.EQ; + case "NE": return VectorOperators.NE; + case "LT": return VectorOperators.LT; + case "LE": return VectorOperators.LE; + case "GT": return VectorOperators.GT; + case "GE": return VectorOperators.GE; + case "ULT": return VectorOperators.ULT; + case "ULE": return VectorOperators.ULE; + case "UGT": return VectorOperators.UGT; + case "UGE": return VectorOperators.UGE; + default: throw new IllegalArgumentException("Unknown comparison operator: " + op); + } + } + + @CompilerControl(CompilerControl.Mode.INLINE) + protected VectorOperators.Comparison comparison_con() throws Throwable { + return (VectorOperators.Comparison) MUTABLE_COMPARISON_CONSTANT_HANDLE.invokeExact(); + } + + // Subclasses with different comparison operators + public static class IntegerComparisons extends MaskCompareNotBenchmark { + @Param({"EQ", "NE", "LT", "LE", "GT", "GE", "ULT", "ULE", "UGT", "UGE"}) + public String COMPARISON_OP; + + @Override + protected String getComparisonOperatorName() { + return COMPARISON_OP; + } + + @Benchmark + public void testCompareMaskNotByte() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + ByteVector bv = ByteVector.fromArray(B_SPECIES, bb, 0); + for (int j = 0; j < ARRAYLEN; j += B_SPECIES.length()) { + ByteVector av = ByteVector.fromArray(B_SPECIES, ba, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotShort() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + ShortVector bv = ShortVector.fromArray(S_SPECIES, sb, 0); + for (int j = 0; j < ARRAYLEN; j += S_SPECIES.length()) { + ShortVector av = ShortVector.fromArray(S_SPECIES, sa, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotInt() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + IntVector bv = IntVector.fromArray(I_SPECIES, ib, 0); + for (int j = 0; j < ARRAYLEN; j += I_SPECIES.length()) { + IntVector av = IntVector.fromArray(I_SPECIES, ia, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotLong() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + LongVector bv = LongVector.fromArray(L_SPECIES, lb, 0); + for (int j = 0; j < ARRAYLEN; j += L_SPECIES.length()) { + LongVector av = LongVector.fromArray(L_SPECIES, la, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + } + + public static class FloatingPointComparisons extends MaskCompareNotBenchmark { + // "ULT", "ULE", "UGT", "UGE" are not supported for floating point types + @Param({"EQ", "NE", "LT", "LE", "GT", "GE"}) + public String COMPARISON_OP; + + @Override + protected String getComparisonOperatorName() { + return COMPARISON_OP; + } + + @Benchmark + public void testCompareMaskNotFloat() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + FloatVector bv = FloatVector.fromArray(F_SPECIES, fb, 0); + for (int j = 0; j < ARRAYLEN; j += F_SPECIES.length()) { + FloatVector av = FloatVector.fromArray(F_SPECIES, fa, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotDouble() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + DoubleVector bv = DoubleVector.fromArray(D_SPECIES, db, 0); + for (int j = 0; j < ARRAYLEN; j += D_SPECIES.length()) { + DoubleVector av = DoubleVector.fromArray(D_SPECIES, da, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + } +} diff --git a/test/micro/org/openjdk/bench/vm/compiler/SerialAdditions.java b/test/micro/org/openjdk/bench/vm/compiler/SerialAdditions.java new file mode 100644 index 00000000000..965b940f417 --- /dev/null +++ b/test/micro/org/openjdk/bench/vm/compiler/SerialAdditions.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2025, Red Hat and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.vm.compiler; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +/** + * Tests speed of adding a series of additions of the same operand. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 4, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(value = 3) +public class SerialAdditions { + private int a = 0xBADB0BA; + private long b = 0x900dba51l; + + @Benchmark + public int addIntsTo02() { + return a + a; // baseline, still a + a + } + + @Benchmark + public int addIntsTo04() { + return a + a + a + a; // a*4 => a<<2 + } + + @Benchmark + public int addIntsTo05() { + return a + a + a + a + a; // a*5 => (a<<2) + a + } + + @Benchmark + public int addIntsTo06() { + return a + a + a + a + a + a; // a*6 => (a<<1) + (a<<2) + } + + @Benchmark + public int addIntsTo08() { + return a + a + a + a + a + a + a + a; // a*8 => a<<3 + } + + @Benchmark + public int addIntsTo16() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a; // a*16 => a<<4 + } + + @Benchmark + public int addIntsTo23() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a; // a*23 + } + + @Benchmark + public int addIntsTo32() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a; // a*32 => a<<5 + } + + @Benchmark + public int addIntsTo42() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a; // a*42 + } + + @Benchmark + public int addIntsTo64() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a; // 64 * a => a << 6 + } + + @Benchmark + public void addIntsMixed(Blackhole blackhole) { + blackhole.consume(addIntsTo02()); + blackhole.consume(addIntsTo04()); + blackhole.consume(addIntsTo05()); + blackhole.consume(addIntsTo06()); + blackhole.consume(addIntsTo08()); + blackhole.consume(addIntsTo16()); + blackhole.consume(addIntsTo23()); + blackhole.consume(addIntsTo32()); + blackhole.consume(addIntsTo42()); + blackhole.consume(addIntsTo64()); + } + + @Benchmark + public long addLongsTo02() { + return b + b; // baseline, still a + a + } + + @Benchmark + public long addLongsTo04() { + return b + b + b + b; // a*4 => a<<2 + } + + @Benchmark + public long addLongsTo05() { + return b + b + b + b + b; // a*5 => (a<<2) + a + } + + @Benchmark + public long addLongsTo06() { + return b + b + b + b + b + b; // a*6 => (a<<1) + (a<<2) + } + + @Benchmark + public long addLongsTo08() { + return b + b + b + b + b + b + b + b; // a*8 => a<<3 + } + + @Benchmark + public long addLongsTo16() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b; // a*16 => a<<4 + } + + @Benchmark + public long addLongsTo23() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b; // a*23 + } + + @Benchmark + public long addLongsTo32() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b; // a*32 => a<<5 + } + + @Benchmark + public long addLongsTo42() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b; // a*42 + } + + @Benchmark + public long addLongsTo64() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b; // 64 * a => a << 6 + } + + @Benchmark + public void addLongsMixed(Blackhole blackhole) { + blackhole.consume(addLongsTo02()); + blackhole.consume(addLongsTo04()); + blackhole.consume(addLongsTo05()); + blackhole.consume(addLongsTo06()); + blackhole.consume(addLongsTo08()); + blackhole.consume(addLongsTo16()); + blackhole.consume(addLongsTo23()); + blackhole.consume(addLongsTo32()); + blackhole.consume(addLongsTo42()); + blackhole.consume(addLongsTo64()); + } +}